From 97134373dcf0f10fa7f42e02f7252337cbdae606 Mon Sep 17 00:00:00 2001 From: nathangray Date: Wed, 4 Apr 2018 10:25:42 -0600 Subject: [PATCH] Refactor sharing tests to use a base class so Collabora tests can exend it --- api/tests/Vfs/SharingBase.php | 505 ++++++++++++++++++++++++++++++++++ api/tests/Vfs/SharingTest.php | 394 +------------------------- 2 files changed, 511 insertions(+), 388 deletions(-) create mode 100644 api/tests/Vfs/SharingBase.php diff --git a/api/tests/Vfs/SharingBase.php b/api/tests/Vfs/SharingBase.php new file mode 100644 index 0000000000..98c6c622a2 --- /dev/null +++ b/api/tests/Vfs/SharingBase.php @@ -0,0 +1,505 @@ + 5 + ); + + public function setUp() + { + + } + + public function tearDown() + { + LoggedInTest::tearDownAfterClass(); + LoggedInTest::setupBeforeClass(); + + // Re-init, since they look at user, fstab, etc. + // Also, further tests that access the filesystem fail if we don't + Vfs::clearstatcache(); + Vfs::init_static(); + Vfs\StreamWrapper::init_static(); + + // Need to ask about mounts, or other tests fail + Vfs::mount(); + + $backup = Vfs::$is_root; + Vfs::$is_root = true; + + // Remove any added files (as root to limit versioning issues) + Vfs::remove($this->files); + + // Remove any mounts + foreach($this->mounts as $mount) + { + Vfs::umount($mount); + } + + // Remove any added shares + foreach($this->shares as $share) + { + Sharing::delete($share); + } + + foreach($this->entries as $entry) + { + list($callback, $params) = $entry; + call_user_func_array($callback, $params); + } + + + Vfs::$is_root = $backup; + } + + + /** + * Check a given directory to see that a link to it works. + * + * We check + * - Files/directories available to original user are available through share + * - Permissions match share (Read / Write) + * - Files are not empty + * + * @param string $dir + * @param string $mode + */ + protected function checkDirectory($dir, $mode) + { + if(static::LOG_LEVEL) + { + echo "\n".__METHOD__ . "($dir, $mode)\n"; + } + $this->files += $this->addFiles($dir); + + $logged_in_files = array_map( + function($path) use ($dir) {return str_replace($dir, '/', $path);}, + Vfs::find($dir, static::VFS_OPTIONS) + ); + + if(static::LOG_LEVEL > 1) + { + echo "\n".$this->getName(); + echo "\nLogged in files:\n".implode("\n", $logged_in_files)."\n"; + } + + // Create and use link + $extra = array(); + switch($mode) + { + case Sharing::WRITABLE: + $extra['share_writable'] = TRUE; + break; + } + $this->shareLink($dir, $mode, $extra); + + $files = Vfs::find('/', static::VFS_OPTIONS); + + if(static::LOG_LEVEL > 1) + { + echo "\nLinked files:\n".implode("\n", $files)."\n"; + } + + // Make sure files are the same + $this->assertEquals($logged_in_files, $files); + + // Make sure all are readonly + foreach($files as $file) + { + $this->checkOneFile($file, $mode); + } + } + + /** + * Check the access permissions for one file/directory + * + * @param string $file + * @param string $mode + */ + protected function checkOneFile($file, $mode) + { + if(static::LOG_LEVEL > 1) + { + $stat = Vfs::stat($file); + echo "\t".Vfs::int2mode($stat['mode'])."\t$file\n"; + } + + // All the test files have something in them + if(!Vfs::is_dir($file)) + { + $this->assertNotEmpty(file_get_contents(Vfs::PREFIX.$file), "$file was empty"); + } + + // Check permissions + switch($mode) + { + case Sharing::READONLY: + $this->assertFalse(Vfs::is_writable($file)); + if(!Vfs::is_dir($file)) + { + // We expect this to fail + $this->assertFalse(@file_put_contents(Vfs::PREFIX.$file, 'Writable check')); + } + break; + case Sharing::WRITABLE: + // Root is not writable + if($file == '/') continue; + + $this->assertTrue(Vfs::is_writable($file), $file . ' was not writable'); + if(!Vfs::is_dir($file)) + { + $this->assertNotFalse(file_put_contents(Vfs::PREFIX.$file, 'Writable check')); + } + break; + } + + } + + /** + * Start versioning for the given path + * + * @param string $path + */ + protected function mountVersioned($path) + { + if (!class_exists('EGroupware\Stylite\Vfs\Versioning\StreamWrapper')) + { + $this->markTestSkipped("No versioning available"); + } + if(substr($path, -1) == '/') $path = substr($path, 0, -1); + $backup = Vfs::$is_root; + Vfs::$is_root = true; + $url = Versioning\StreamWrapper::PREFIX.$path; + $this->assertTrue(Vfs::mount($url,$path), "Unable to mount $path as versioned"); + Vfs::$is_root = $backup; + + $this->mounts[] = $path; + Vfs::clearstatcache(); + Vfs::init_static(); + Vfs\StreamWrapper::init_static(); + } + + /** + * Mount a test filesystem path (api/tests/fixtures/Vfs/filesystem_mount) + * at the given VFS path + * + * @param string $path + */ + protected function mountFilesystem($path) + { + // Vfs breaks if path has trailing / + if(substr($path, -1) == '/') $path = substr($path, 0, -1); + + $backup = Vfs::$is_root; + Vfs::$is_root = true; + $url = Filesystem\StreamWrapper::SCHEME.'://default'. realpath(__DIR__ . '/../fixtures/Vfs/filesystem_mount'). '?group=Default&mode=775'; + $this->assertTrue(Vfs::mount($url,$path), "Unable to mount $url to $path"); + Vfs::$is_root = $backup; + + $this->mounts[] = $path; + Vfs::clearstatcache(); + Vfs::init_static(); + Vfs\StreamWrapper::init_static(); + } + + /** + * Merge a test filesystem path (api/tests/fixtures/Vfs/filesystem_mount) + * with the given VFS path + * + * @param string $path + */ + protected function mountMerge($path) + { + // Vfs breaks if path has trailing / + if(substr($path, -1) == '/') $path = substr($path, 0, -1); + + $backup = Vfs::$is_root; + Vfs::$is_root = true; + $url = \EGroupware\Stylite\Vfs\Merge\StreamWrapper::SCHEME.'://default'.$path.'?merge=api/tests/fixtures/Vfs/filesystem_mount'; + $this->assertTrue(Vfs::mount($url,$path), "Unable to mount $url to $path"); + Vfs::$is_root = $backup; + + $this->mounts[] = $path; + Vfs::clearstatcache(); + Vfs::init_static(); + Vfs\StreamWrapper::init_static(); + } + + /** + * Add some files to the given path so there's something to find. + * + * @param string $path + * + * @return array of paths + */ + protected function addFiles($path) + { + $files = array(); + + // Plain file + $files[] = $file = $path.'test_file.txt'; + $this->assertTrue( + file_put_contents(Vfs::PREFIX.$file, 'Test for ' . $this->getName() ."\n". Api\DateTime::to()) !== FALSE, + 'Unable to write test file - check file permissions for CLI user' + ); + + // Subdirectory + $files[] = $dir = $path.'sub_dir/'; + if(Vfs::is_dir($dir)) + { + Vfs::remove($dir); + } + $this->assertTrue( + Vfs::mkdir($dir), + 'Unable to create subdirectory ' . $dir + ); + + // File in a subdirectory + $files[] = $file = $dir.'subdir_test_file.txt'; + $this->assertTrue( + file_put_contents(Vfs::PREFIX.$file, 'Test for ' . $this->getName() ."\n". Api\DateTime::to()) !== FALSE, + 'Unable to write test file - check file permissions for CLI user' + ); + + // Symlinked file + // Always says its empty + $files[] = $symlink = $path.'symlink.txt'; + if(Vfs::file_exists($symlink)) Vfs::remove($symlink); + $this->assertTrue( + Vfs::symlink($file, $symlink), + 'Unable to create symlink' + ); +/* + // Symlinked dir + $files[] = $symlinked_dir = $path.'sym_dir/'; + $this->assertTrue( + Vfs::symlink($dir, $symlinked_dir), + 'Unable to create symlinked directory ' . $symlinked_dir + ); +*/ + return $files; + } + + /** + * Test that readable shares are actually readable + * + * @param string $path + */ + public function createShare($path, $mode, $extra = array()) + { + // Make sure the path is there + if(!Vfs::is_readable($path)) + { + $this->assertTrue(Vfs::is_dir($path) ? Vfs::mkdir($path,0750,true) : Vfs::touch($path)); + } + + // Create share + $this->shares[] = $share = Sharing::create($path, $mode, $name, $recipients, $extra); + + return $share; + } + + /** + * Make an infolog entry + */ + protected function make_infolog() + { + $bo = new \infolog_bo(); + $element = array( + 'info_subject' => "Test infolog for #{$this->getName()}", + 'info_des' => 'Test element for ' . $this->getName() ."\n". Api\DateTime::to(), + 'info_status' => 'open' + ); + + $element_id = $bo->write($element, true, true, true, true); + return $element_id; + } + + /** + * Test that a share link can be made, and that only that path is available + * + * @param string $path + */ + public function shareLink($path, $mode, $extra = array()) + { + if(static::LOG_LEVEL > 1) + { + echo __METHOD__ . "('$path',$mode)\n"; + } + // Setup - create path and share + $share = $this->createShare($path, $mode, $extra); + $link = Vfs\Sharing::share2link($share); + //echo __METHOD__ . " link: $link\n"; + + if(static::LOG_LEVEL) + { + echo __METHOD__ . " share: " . array2string($share)."\n"; + } + + // Setup for share to load + $_SERVER['REQUEST_URI'] = $link; + preg_match('|^https?://[^/]+(/.*)share.php/'.$share['share_token'].'$|', $path_info=$_SERVER['REQUEST_URI'], $matches); + $_SERVER['SCRIPT_NAME'] = $matches[1]; + + // Log out & clear cache + LoggedInTest::tearDownAfterClass(); + + // Re-init, since they look at user, fstab, etc. + // Also, further tests that access the filesystem fail if we don't + Vfs::clearstatcache(); + Vfs::init_static(); + Vfs\StreamWrapper::init_static(); + + + // If it's a directory, check to make sure it gives the filemanager UI + if(Vfs::is_dir($path)) + { + $this->checkDirectoryLink($link, $share); + } + + // Load share + $this->setup_info(); + + + if(static::LOG_LEVEL > 1) + { + echo "Sharing mounts:\n"; + var_dump(Vfs::mount()); + } + + // Our path should be mounted to root + $this->assertTrue(Vfs::is_readable('/'), 'Could not read root (/) from link'); + + // Check other paths + $this->assertFalse(Vfs::is_readable($path)); + $this->assertFalse(Vfs::is_readable($path . '../')); + } + + /** + * Test to make sure that a directory link leads to a limited filemanager + * interface (not a file or 404). + * + * @param type $link + * @param type $share + */ + public function checkDirectoryLink($link, $share) + { + // Set up curl + $curl = curl_init($link); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + $html = curl_exec($curl); + curl_close($curl); + + if(!$html) + { + // No response - could mean something is terribly wrong, or it could + // mean we're running on Travis with no webserver to answer the + // request + return; + } + + // Parse & check for nextmatch + $dom = new \DOMDocument(); + @$dom->loadHTML($html); + $xpath = new \DOMXPath($dom); + $form = $xpath->query ('//form')->item(0); + if(!$form && static::LOG_LEVEL) + { + echo "Didn't find filemanager interface\n"; + if(static::LOG_LEVEL > 1) + { + echo $form."\n\n"; + } + } + $data = json_decode($form->getAttribute('data-etemplate')); + + $this->assertEquals('filemanager.index', $data->name); + + // Make sure we start at root, not somewhere else like the token mounted + // as a sub-directory + $this->assertEquals('/', $data->data->content->nm->path); + + unset($data->data->content->nm->actions); + //var_dump($data->data->content->nm); + } + protected function setup_info() + { + // Copied from share.php + $GLOBALS['egw_info'] = array( + 'flags' => array( + 'disable_Template_class' => true, + 'noheader' => true, + 'nonavbar' => 'always', // true would cause eTemplate to reset it to false for non-popups! + 'currentapp' => 'filemanager', + 'autocreate_session_callback' => 'EGroupware\\Api\\Vfs\\Sharing::create_session', + 'no_exception_handler' => 'basic_auth', // we use a basic auth exception handler (sends exception message as basic auth realm) + ) + ); + + ob_start(); + static::load_egw('anonymous','','',$GLOBALS['egw_info']); + if(static::LOG_LEVEL > 1) + { + ob_end_flush(); + } + else + { + ob_end_clean(); + } + } +} diff --git a/api/tests/Vfs/SharingTest.php b/api/tests/Vfs/SharingTest.php index 6fc07c2c67..774c929ea8 100644 --- a/api/tests/Vfs/SharingTest.php +++ b/api/tests/Vfs/SharingTest.php @@ -3,12 +3,14 @@ /** * Tests for sharing files and directories * - * This is a bit of a mess, but I think we probably want to automatically test - * this to make sure we don't expose more than desired. + * We systematically test the various Vfs backends with files, subdirectories + * and symlinks. A backend is mounted (if needed) and test files are created. + * Then we create the share (readable, writable) log out and check what the share + * gives. This is compared against what should be available, and we check the + * access on each. * * @link http://www.egroupware.org * @author Nathan Gray - * @package * @copyright (c) 2018 Nathan Gray * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License */ @@ -21,7 +23,7 @@ use EGroupware\Api\LoggedInTest as LoggedInTest; use EGroupware\Stylite\Vfs\Versioning; -class SharingTest extends LoggedInTest +class SharingTest extends SharingBase { /** * How much should be logged to the console (stdout) @@ -293,388 +295,4 @@ class SharingTest extends LoggedInTest // It should be picked up normally, but an explicit check can't hurt $this->checkOneFile('/filesystem_test.txt', Sharing::WRITABLE); } - - /** - * Check a given directory to see that a link to it works. - * - * We check - * - Files/directories available to original user are available through share - * - Permissions match share (Read / Write) - * - Files are not empty - * - * @param string $dir - * @param string $mode - */ - protected function checkDirectory($dir, $mode) - { - if(static::LOG_LEVEL) - { - echo "\n".__METHOD__ . "($dir, $mode)\n"; - } - $this->files += $this->addFiles($dir); - - $logged_in_files = array_map( - function($path) use ($dir) {return str_replace($dir, '/', $path);}, - Vfs::find($dir, static::VFS_OPTIONS) - ); - - if(static::LOG_LEVEL > 1) - { - echo "\n".$this->getName(); - echo "\nLogged in files:\n".implode("\n", $logged_in_files)."\n"; - } - - // Create and use link - $extra = array(); - switch($mode) - { - case Sharing::WRITABLE: - $extra['share_writable'] = TRUE; - break; - } - $this->shareLink($dir, $mode, $extra); - - $files = Vfs::find('/', static::VFS_OPTIONS); - - if(static::LOG_LEVEL > 1) - { - echo "\nLinked files:\n".implode("\n", $files)."\n"; - } - - // Make sure files are the same - $this->assertEquals($logged_in_files, $files); - - // Make sure all are readonly - foreach($files as $file) - { - $this->checkOneFile($file, $mode); - } - } - - /** - * Check the access permissions for one file/directory - * - * @param string $file - * @param string $mode - */ - protected function checkOneFile($file, $mode) - { - if(static::LOG_LEVEL > 1) - { - $stat = Vfs::stat($file); - echo "\t".Vfs::int2mode($stat['mode'])."\t$file\n"; - } - - // All the test files have something in them - if(!Vfs::is_dir($file)) - { - $this->assertNotEmpty(file_get_contents(Vfs::PREFIX.$file), "$file was empty"); - } - - // Check permissions - switch($mode) - { - case Sharing::READONLY: - $this->assertFalse(Vfs::is_writable($file)); - if(!Vfs::is_dir($file)) - { - // We expect this to fail - $this->assertFalse(@file_put_contents(Vfs::PREFIX.$file, 'Writable check')); - } - break; - case Sharing::WRITABLE: - // Root is not writable - if($file == '/') continue; - - $this->assertTrue(Vfs::is_writable($file), $file . ' was not writable'); - if(!Vfs::is_dir($file)) - { - $this->assertNotFalse(file_put_contents(Vfs::PREFIX.$file, 'Writable check')); - } - break; - } - - } - - /** - * Start versioning for the given path - * - * @param string $path - */ - protected function mountVersioned($path) - { - if (!class_exists('EGroupware\Stylite\Vfs\Versioning\StreamWrapper')) - { - $this->markTestSkipped("No versioning available"); - } - if(substr($path, -1) == '/') $path = substr($path, 0, -1); - $backup = Vfs::$is_root; - Vfs::$is_root = true; - $url = Versioning\StreamWrapper::PREFIX.$path; - $this->assertTrue(Vfs::mount($url,$path), "Unable to mount $path as versioned"); - Vfs::$is_root = $backup; - - $this->mounts[] = $path; - Vfs::clearstatcache(); - Vfs::init_static(); - Vfs\StreamWrapper::init_static(); - } - - protected function mountFilesystem($path) - { - // Vfs breaks if path has trailing / - if(substr($path, -1) == '/') $path = substr($path, 0, -1); - - $backup = Vfs::$is_root; - Vfs::$is_root = true; - $url = Filesystem\StreamWrapper::SCHEME.'://default'. realpath(__DIR__ . '/../fixtures/Vfs/filesystem_mount'). '?group=Default&mode=775'; - $this->assertTrue(Vfs::mount($url,$path), "Unable to mount $url to $path"); - Vfs::$is_root = $backup; - - $this->mounts[] = $path; - Vfs::clearstatcache(); - Vfs::init_static(); - Vfs\StreamWrapper::init_static(); - } - - protected function mountMerge($path) - { - // Vfs breaks if path has trailing / - if(substr($path, -1) == '/') $path = substr($path, 0, -1); - - $backup = Vfs::$is_root; - Vfs::$is_root = true; - $url = \EGroupware\Stylite\Vfs\Merge\StreamWrapper::SCHEME.'://default'.$path.'?merge=api/tests/fixtures/Vfs/filesystem_mount'; - $this->assertTrue(Vfs::mount($url,$path), "Unable to mount $url to $path"); - Vfs::$is_root = $backup; - - $this->mounts[] = $path; - Vfs::clearstatcache(); - Vfs::init_static(); - Vfs\StreamWrapper::init_static(); - } - - /** - * Add some files to the given path so there's something to find. - * - * @param string $path - * - * @return array of paths - */ - protected function addFiles($path) - { - $files = array(); - - // Plain file - $files[] = $file = $path.'test_file.txt'; - $this->assertTrue( - file_put_contents(Vfs::PREFIX.$file, 'Test for ' . $this->getName() ."\n". Api\DateTime::to()) !== FALSE, - 'Unable to write test file - check file permissions for CLI user' - ); - - // Subdirectory - $files[] = $dir = $path.'sub_dir/'; - if(Vfs::is_dir($dir)) - { - Vfs::remove($dir); - } - $this->assertTrue( - Vfs::mkdir($dir), - 'Unable to create subdirectory ' . $dir - ); - - // File in a subdirectory - $files[] = $file = $dir.'subdir_test_file.txt'; - $this->assertTrue( - file_put_contents(Vfs::PREFIX.$file, 'Test for ' . $this->getName() ."\n". Api\DateTime::to()) !== FALSE, - 'Unable to write test file - check file permissions for CLI user' - ); - - // Symlinked file - /* Always says its empty - $files[] = $symlink = $path.'symlink.txt'; - if(Vfs::file_exists($symlink)) Vfs::remove($symlink); - $this->assertTrue( - Vfs::symlink($file, $symlink), - 'Unable to create symlink' - ); - - // Symlinked dir - $files[] = $symlinked_dir = $path.'sym_dir/'; - $this->assertTrue( - Vfs::symlink($dir, $symlinked_dir), - 'Unable to create symlinked directory ' . $symlinked_dir - ); -*/ - return $files; - } - - /** - * Test that readable shares are actually readable - * - * @param string $path - */ - public function createShare($path, $mode, $extra = array()) - { - // Make sure the path is there - if(!Vfs::is_readable($path)) - { - $this->assertTrue(Vfs::is_dir($path) ? Vfs::mkdir($path,0750,true) : Vfs::touch($path)); - } - - // Create share - $this->shares[] = $share = Sharing::create($path, $mode, $name, $recipients, $extra); - - return $share; - } - - /** - * Make an infolog entry - */ - protected function make_infolog() - { - $bo = new \infolog_bo(); - $element = array( - 'info_subject' => "Test infolog for #{$this->getName()}", - 'info_des' => 'Test element for ' . $this->getName() ."\n". Api\DateTime::to(), - 'info_status' => 'open' - ); - - $element_id = $bo->write($element, true, true, true, true); - return $element_id; - } - - /** - * Test that a share link can be made, and that only that path is available - * - * @param string $path - */ - public function shareLink($path, $mode, $extra = array()) - { - if(static::LOG_LEVEL > 1) - { - echo __METHOD__ . "('$path',$mode)\n"; - } - // Setup - create path and share - $share = $this->createShare($path, $mode, $extra); - $link = Vfs\Sharing::share2link($share); - //echo __METHOD__ . " link: $link\n"; - - if(static::LOG_LEVEL) - { - echo __METHOD__ . " share: " . array2string($share)."\n"; - } - - // Setup for share to load - $_SERVER['REQUEST_URI'] = $link; - preg_match('|^https?://[^/]+(/.*)share.php/'.$share['share_token'].'$|', $path_info=$_SERVER['REQUEST_URI'], $matches); - $_SERVER['SCRIPT_NAME'] = $matches[1]; - - // Log out & clear cache - LoggedInTest::tearDownAfterClass(); - - // Re-init, since they look at user, fstab, etc. - // Also, further tests that access the filesystem fail if we don't - Vfs::clearstatcache(); - Vfs::init_static(); - Vfs\StreamWrapper::init_static(); - - - // If it's a directory, check to make sure it gives the filemanager UI - if(Vfs::is_dir($path)) - { - $this->checkDirectoryLink($link, $share); - } - - // Load share - $this->setup_info(); - - - if(static::LOG_LEVEL > 1) - { - echo "Sharing mounts:\n"; - var_dump(Vfs::mount()); - } - - // Our path should be mounted to root - $this->assertTrue(Vfs::is_readable('/'), 'Could not read root (/) from link'); - - // Check other paths - $this->assertFalse(Vfs::is_readable($path)); - $this->assertFalse(Vfs::is_readable($path . '../')); - } - - /** - * Test to make sure that a directory link leads to a limited filemanager - * interface (not a file or 404). - * - * @param type $link - * @param type $share - */ - public function checkDirectoryLink($link, $share) - { - // Set up curl - $curl = curl_init($link); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); - $html = curl_exec($curl); - curl_close($curl); - - if(!$html) - { - // No response - could mean something is terribly wrong, or it could - // mean we're running on Travis with no webserver to answer the - // request - return; - } - - // Parse & check for nextmatch - $dom = new \DOMDocument(); - @$dom->loadHTML($html); - $xpath = new \DOMXPath($dom); - $form = $xpath->query ('//form')->item(0); - if(!$form && static::LOG_LEVEL) - { - echo "Didn't find filemanager interface\n"; - if(static::LOG_LEVEL > 1) - { - echo $form."\n\n"; - } - } - $data = json_decode($form->getAttribute('data-etemplate')); - - $this->assertEquals('filemanager.index', $data->name); - - // Make sure we start at root, not somewhere else like the token mounted - // as a sub-directory - $this->assertEquals('/', $data->data->content->nm->path); - - unset($data->data->content->nm->actions); - //var_dump($data->data->content->nm); - } - protected function setup_info() - { - // Copied from share.php - $GLOBALS['egw_info'] = array( - 'flags' => array( - 'disable_Template_class' => true, - 'noheader' => true, - 'nonavbar' => 'always', // true would cause eTemplate to reset it to false for non-popups! - 'currentapp' => 'filemanager', - 'autocreate_session_callback' => 'EGroupware\\Api\\Vfs\\Sharing::create_session', - 'no_exception_handler' => 'basic_auth', // we use a basic auth exception handler (sends exception message as basic auth realm) - ) - ); - - ob_start(); - static::load_egw('anonymous','','',$GLOBALS['egw_info']); - if(static::LOG_LEVEL > 1) - { - ob_end_flush(); - } - else - { - ob_end_clean(); - } - } }