public function tearDown() { parent::tearDown(); i18n::set_locale($this->originalLocale); Config::inst()->remove('SilverStripe\\Forms\\TimeField', 'default_config'); Config::inst()->update('SilverStripe\\Forms\\TimeField', 'default_config', $this->origTimeConfig); }
/** * Triggered early in the request when a flush is requested */ public static function flush() { $disabled = Config::inst()->get(static::class, 'disable_flush_combined'); if (!$disabled) { self::delete_all_combined_files(); } }
/** * Register a new batch action. Each batch action needs to be represented by a subclass * of {@link CMSBatchAction}. * * @param string $urlSegment The URL Segment of the batch action - the URL used to process this * action will be admin/pages/batchactions/(urlSegment) * @param string $batchActionClass The name of the CMSBatchAction subclass to register * @param string $recordClass */ public static function register($urlSegment, $batchActionClass, $recordClass = SiteTree::class) { if (!is_subclass_of($batchActionClass, CMSBatchAction::class)) { throw new InvalidArgumentException("CMSBatchActionHandler::register() - Bad class '{$batchActionClass}'"); } Config::inst()->update(CMSBatchActionHandler::class, 'batch_actions', array($urlSegment => array('class' => $batchActionClass, 'recordClass' => $recordClass))); }
public function connect($parameters, $selectDB = false) { // Normally $selectDB is set to false by the MySQLDatabase controller, as per convention $selectedDB = $selectDB && !empty($parameters['database']) ? $parameters['database'] : null; // Connection charset and collation $connCharset = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'connection_charset'); $connCollation = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'connection_collation'); if (!empty($parameters['port'])) { $this->dbConn = new MySQLi($parameters['server'], $parameters['username'], $parameters['password'], $selectedDB, $parameters['port']); } else { $this->dbConn = new MySQLi($parameters['server'], $parameters['username'], $parameters['password'], $selectedDB); } if ($this->dbConn->connect_error) { $this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error); } // Set charset and collation if given and not null. Can explicitly set to empty string to omit $charset = isset($parameters['charset']) ? $parameters['charset'] : $connCharset; if (!empty($charset)) { $this->dbConn->set_charset($charset); } $collation = isset($parameters['collation']) ? $parameters['collation'] : $connCollation; if (!empty($collation)) { $this->dbConn->query("SET collation_connection = {$collation}"); } }
public function setUp() { parent::setUp(); $this->logInWithPermission('ADMIN'); // Save versioned state $this->oldReadingMode = Versioned::get_reading_mode(); Versioned::set_stage(Versioned::DRAFT); // Set backend root to /UploadFieldTest AssetStoreTest_SpyStore::activate('UploadFieldTest'); // Set the File Name Filter replacements so files have the expected names Config::inst()->update('SilverStripe\\Assets\\FileNameFilter', 'default_replacements', array('/\\s/' => '-', '/_/' => '-', '/[^A-Za-z0-9+.\\-]+/' => '', '/[\\-]{2,}/' => '-', '/^[\\.\\-_]+/' => '')); // Create a test folders for each of the fixture references foreach (Folder::get() as $folder) { $path = AssetStoreTest_SpyStore::getLocalPath($folder); Filesystem::makeFolder($path); } // Create a test files for each of the fixture references $files = File::get()->exclude('ClassName', 'SilverStripe\\Assets\\Folder'); foreach ($files as $file) { $path = AssetStoreTest_SpyStore::getLocalPath($file); Filesystem::makeFolder(dirname($path)); $fh = fopen($path, "w+"); fwrite($fh, str_repeat('x', 1000000)); fclose($fh); } }
/** * Get maximum file size for all or specified file extension. * * @param string $ext * @return int Filesize in bytes */ public function getAllowedMaxFileSize($ext = null) { // Check if there is any defined instance max file sizes if (empty($this->allowedMaxFileSize)) { // Set default max file sizes if there isn't $fileSize = Config::inst()->get('SilverStripe\\Assets\\Upload_Validator', 'default_max_file_size'); if ($fileSize) { $this->setAllowedMaxFileSize($fileSize); } else { // When no default is present, use maximum set by PHP $maxUpload = File::ini2bytes(ini_get('upload_max_filesize')); $maxPost = File::ini2bytes(ini_get('post_max_size')); $this->setAllowedMaxFileSize(min($maxUpload, $maxPost)); } } $ext = strtolower($ext); if ($ext) { if (isset($this->allowedMaxFileSize[$ext])) { return $this->allowedMaxFileSize[$ext]; } $category = File::get_app_category($ext); if ($category && isset($this->allowedMaxFileSize['[' . $category . ']'])) { return $this->allowedMaxFileSize['[' . $category . ']']; } } return isset($this->allowedMaxFileSize['*']) ? $this->allowedMaxFileSize['*'] : false; }
public function setUp() { parent::setUp(); // Set backend AssetStoreTest_SpyStore::activate('DBFileTest'); Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', '/mysite/'); }
public function testNice() { $time = DBField::create_field('Time', '17:15:55'); $this->assertEquals('5:15pm', $time->Nice()); Config::inst()->update('SilverStripe\\ORM\\FieldType\\DBTime', 'nice_format', 'H:i:s'); $this->assertEquals('17:15:55', $time->Nice()); }
/** * Configure server files for this store * * @param bool $forceOverwrite Force regeneration even if files already exist * @throws \Exception */ protected function configureServer($forceOverwrite = false) { // Get server type $type = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '*'; list($type) = explode('/', strtolower($type)); // Determine configurations to write $rules = Config::inst()->get(get_class($this), 'server_configuration', Config::FIRST_SET); if (empty($rules[$type])) { return; } $configurations = $rules[$type]; // Apply each configuration $config = new FlysystemConfig(); $config->set('visibility', 'private'); foreach ($configurations as $file => $template) { if ($forceOverwrite || !$this->has($file)) { // Evaluate file $content = $this->renderTemplate($template); $success = $this->write($file, $content, $config); if (!$success) { throw new \Exception("Error writing server configuration file \"{$file}\""); } } } }
public function testEnablePluginsByArrayWithPaths() { Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', 'http://mysite.com/subdir'); $c = new TinyMCEConfig(); $c->setTheme('modern'); $c->setOption('language', 'es'); $c->disablePlugins('table', 'emoticons', 'paste', 'code', 'link', 'importcss'); $c->enablePlugins(array('plugin1' => 'mypath/plugin1.js', 'plugin2' => '/anotherbase/mypath/plugin2.js', 'plugin3' => 'https://www.google.com/plugin.js', 'plugin4' => null, 'plugin5' => null)); $attributes = $c->getAttributes(); $config = Convert::json2array($attributes['data-config']); $plugins = $config['external_plugins']; $this->assertNotEmpty($plugins); // Plugin specified via relative url $this->assertContains('plugin1', array_keys($plugins)); $this->assertEquals('http://mysite.com/subdir/mypath/plugin1.js', $plugins['plugin1']); // Plugin specified via root-relative url $this->assertContains('plugin2', array_keys($plugins)); $this->assertEquals('http://mysite.com/anotherbase/mypath/plugin2.js', $plugins['plugin2']); // Plugin specified with absolute url $this->assertContains('plugin3', array_keys($plugins)); $this->assertEquals('https://www.google.com/plugin.js', $plugins['plugin3']); // Plugin specified with standard location $this->assertContains('plugin4', array_keys($plugins)); $this->assertEquals('http://mysite.com/subdir/' . ADMIN_THIRDPARTY_DIR . '/tinymce/plugins/plugin4/plugin.min.js', $plugins['plugin4']); // Check that internal plugins are extractable separately $this->assertEquals(['plugin4', 'plugin5'], $c->getInternalPlugins()); // Test plugins included via gzip compresser HTMLEditorField::config()->update('use_gzip', true); $this->assertEquals(ADMIN_THIRDPARTY_DIR . '/tinymce/tiny_mce_gzip.php?js=1&plugins=plugin4,plugin5&themes=modern&languages=es&diskcache=true&src=true', $c->getScriptURL()); // If gzip is disabled only the core plugin is loaded HTMLEditorField::config()->remove('use_gzip'); $this->assertEquals(ADMIN_THIRDPARTY_DIR . '/tinymce/tinymce.min.js', $c->getScriptURL()); }
public function requireField() { // @todo: Remove mysql-centric logic from this $charset = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'charset'); $collation = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'collation'); $values = array('type' => 'set', 'parts' => array('enums' => $this->enum, 'character set' => $charset, 'collate' => $collation, 'default' => $this->default, 'table' => $this->tableName, 'arrayValue' => $this->arrayValue)); DB::require_field($this->tableName, $this->name, $values); }
/** * (non-PHPdoc) * @see DBField::requireField() */ public function requireField() { $charset = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'charset'); $collation = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'collation'); $parts = array('datatype' => 'varchar', 'precision' => $this->size, 'character set' => $charset, 'collate' => $collation, 'arrayValue' => $this->arrayValue); $values = array('type' => 'varchar', 'parts' => $parts); DB::require_field($this->tableName, $this->name, $values); }
public function setUp() { parent::setUp(); $this->rootDir = ASSETS_PATH . '/AssetAdapterTest'; Filesystem::makeFolder($this->rootDir); Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', '/'); $this->originalServer = $_SERVER; }
/** * (non-PHPdoc) * @see DBField::requireField() */ public function requireField() { $charset = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'charset'); $collation = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'collation'); $parts = ['datatype' => 'mediumtext', 'character set' => $charset, 'collate' => $collation, 'default' => $this->defaultVal, 'arrayValue' => $this->arrayValue]; $values = ['type' => 'text', 'parts' => $parts]; DB::require_field($this->tableName, $this->name, $values); }
public function testPermissionFieldRespectsHiddenPermissions() { $this->session()->inst_set('loggedInAs', $this->idFromFixture('SilverStripe\\Security\\Member', 'admin')); $group = $this->objFromFixture('SilverStripe\\Security\\Group', 'admin'); Config::inst()->update('SilverStripe\\Security\\Permission', 'hidden_permissions', array('CMS_ACCESS_ReportAdmin')); $response = $this->get(sprintf('admin/security/EditForm/field/Groups/item/%d/edit', $group->ID)); $this->assertContains('CMS_ACCESS_SecurityAdmin', $response->getBody()); $this->assertNotContains('CMS_ACCESS_ReportAdmin', $response->getBody()); }
public function setUpOnce() { Config::nest(); VersionableExtensionsTest_DataObject::add_extension('SilverStripe\\ORM\\Versioning\\Versioned'); VersionableExtensionsTest_DataObject::add_extension('VersionableExtensionsTest_Extension'); $cfg = Config::inst(); $cfg->update('VersionableExtensionsTest_DataObject', 'versionableExtensions', array('VersionableExtensionsTest_Extension' => array('test1', 'test2', 'test3'))); parent::setUpOnce(); }
/** * Retrieves the config for a named service without performing a hierarchy walk * * @param string $name Name of service * @return mixed Get config for this service */ protected function configFor($name) { // Return cached result if (array_key_exists($name, $this->configs)) { return $this->configs[$name]; } $config = Config::inst()->get('SilverStripe\\Core\\Injector\\Injector', $name); $this->configs[$name] = $config; return $config; }
public function setUp() { parent::setUp(); if (!extension_loaded("gd")) { $this->markTestSkipped("The GD extension is required"); return; } /** @skipUpgrade */ Config::inst()->update('SilverStripe\\Core\\Injector\\Injector', 'Image_Backend', 'SilverStripe\\Assets\\GDBackend'); }
public function setUp() { parent::setUp(); if (!extension_loaded("imagick")) { $this->markTestSkipped("The Imagick extension is not available."); return; } /** @skipUpgrade */ Config::inst()->update('SilverStripe\\Core\\Injector\\Injector', 'Image_Backend', 'SilverStripe\\Assets\\ImagickBackend'); }
public function testTimeFormatDefaultCheckedInFormField() { Config::inst()->update('SilverStripe\\i18n\\i18n', 'time_format', 'h:mm:ss a'); $field = $this->createTimeFormatFieldForMember($this->objFromFixture('SilverStripe\\Security\\Member', 'noformatmember')); /** @skipUpgrade */ $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form $parser = new CSSContentParser($field->Field()); $xmlArr = $parser->getBySelector('#Form_Form_TimeFormat_h:mm:ss_a'); $this->assertEquals('checked', (string) $xmlArr[0]['checked']); }
protected function findRoot($root) { // Use explicitly defined path if ($root) { return parent::findRoot($root); } // Use environment defined path if (defined('SS_PROTECTED_ASSETS_PATH')) { return SS_PROTECTED_ASSETS_PATH; } // Default location is under assets return ASSETS_PATH . '/' . Config::inst()->get(get_class($this), 'secure_folder'); }
function testValidAlternativeDatabaseName() { $prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_'; Config::inst()->update('SilverStripe\\Control\\Director', 'environment_type', 'dev'); $this->assertTrue(DB::valid_alternative_database_name($prefix . 'tmpdb1234567')); $this->assertFalse(DB::valid_alternative_database_name($prefix . 'tmpdb12345678')); $this->assertFalse(DB::valid_alternative_database_name('tmpdb1234567')); $this->assertFalse(DB::valid_alternative_database_name('random')); $this->assertFalse(DB::valid_alternative_database_name('')); Config::inst()->update('SilverStripe\\Control\\Director', 'environment_type', 'live'); $this->assertFalse(DB::valid_alternative_database_name($prefix . 'tmpdb1234567')); Config::inst()->update('SilverStripe\\Control\\Director', 'environment_type', 'dev'); }
public function setUp() { parent::setUp(); // Set backend root to /HTMLEditorFieldTest AssetStoreTest_SpyStore::activate('HTMLEditorFieldTest'); // Set the File Name Filter replacements so files have the expected names Config::inst()->update('SilverStripe\\Assets\\FileNameFilter', 'default_replacements', array('/\\s/' => '-', '/_/' => '-', '/[^A-Za-z0-9+.\\-]+/' => '', '/[\\-]{2,}/' => '-', '/^[\\.\\-_]+/' => '')); // Create a test files for each of the fixture references $files = File::get()->exclude('ClassName', 'SilverStripe\\Assets\\Folder'); foreach ($files as $file) { $fromPath = FRAMEWORK_PATH . '/tests/forms/images/' . $file->Name; $destPath = AssetStoreTest_SpyStore::getLocalPath($file); // Only correct for test asset store Filesystem::makeFolder(dirname($destPath)); copy($fromPath, $destPath); } }
public function testNiceDate() { $this->assertEquals('31/03/2008', DBField::create_field('Date', 1206968400)->Nice(), "Date->Nice() works with timestamp integers"); $this->assertEquals('30/03/2008', DBField::create_field('Date', 1206882000)->Nice(), "Date->Nice() works with timestamp integers"); $this->assertEquals('31/03/2008', DBField::create_field('Date', '1206968400')->Nice(), "Date->Nice() works with timestamp strings"); $this->assertEquals('30/03/2008', DBField::create_field('Date', '1206882000')->Nice(), "Date->Nice() works with timestamp strings"); $this->assertEquals('04/03/2003', DBField::create_field('Date', '4/3/03')->Nice(), "Date->Nice() works with D/M/YY format"); $this->assertEquals('04/03/2003', DBField::create_field('Date', '04/03/03')->Nice(), "Date->Nice() works with DD/MM/YY format"); $this->assertEquals('04/03/2003', DBField::create_field('Date', '4/3/03')->Nice(), "Date->Nice() works with D/M/YY format"); $this->assertEquals('04/03/2003', DBField::create_field('Date', '4/03/03')->Nice(), "Date->Nice() works with D/M/YY format"); $this->assertEquals('04/03/2003', DBField::create_field('Date', '4/3/2003')->Nice(), "Date->Nice() works with D/M/YYYY format"); $this->assertEquals('04/03/2003', DBField::create_field('Date', '4-3-2003')->Nice(), "Date->Nice() works with D-M-YYYY format"); $this->assertEquals('04/03/2003', DBField::create_field('Date', '2003-03-04')->Nice(), "Date->Nice() works with YYYY-MM-DD format"); $this->assertEquals('04/03/2003', DBField::create_field('Date', '04/03/2003')->Nice(), "Date->Nice() works with DD/MM/YYYY format"); $this->assertEquals('04/03/2003', DBField::create_field('Date', '04-03-2003')->Nice(), "Date->Nice() works with DD/MM/YYYY format"); $date = DBField::create_field('Date', '2003-03-04'); Config::inst()->update('SilverStripe\\ORM\\FieldType\\DBDate', 'nice_format', 'd F Y'); $this->assertEquals('04 March 2003', $date->Nice()); }
/** * This implementation allows for a list of columns to be passed into MATCH() instead of just one. * * @example * <code> * MyDataObject::get()->filter('SearchFields:fulltext', 'search term') * </code> * * @throws Exception * @return string */ public function getDbName() { $indexes = Config::inst()->get($this->model, "indexes"); if (is_array($indexes) && array_key_exists($this->getName(), $indexes)) { $index = $indexes[$this->getName()]; if (is_array($index) && array_key_exists("value", $index)) { return $this->prepareColumns($index['value']); } else { // Parse a fulltext string (eg. fulltext ("ColumnA", "ColumnB")) to figure out which columns // we need to search. if (preg_match('/^fulltext\\s+\\((.+)\\)$/i', $index, $matches)) { return $this->prepareColumns($matches[1]); } else { throw new Exception(sprintf("Invalid fulltext index format for '%s' on '%s'", $this->getName(), $this->model)); } } } return parent::getDbName(); }
public function process($item, $arguments = null, $scope = null) { $hash = sha1($this->content); $cacheFile = TEMP_FOLDER . "/.cache.{$hash}"; if (!file_exists($cacheFile) || isset($_GET['flush'])) { $content = $this->parseTemplateContent($this->content, "string sha1={$hash}"); $fh = fopen($cacheFile, 'w'); fwrite($fh, $content); fclose($fh); } $val = $this->includeGeneratedTemplate($cacheFile, $item, $arguments, null, $scope); if ($this->cacheTemplate !== null) { $cacheTemplate = $this->cacheTemplate; } else { $cacheTemplate = Config::inst()->get('SilverStripe\\View\\SSViewer_FromString', 'cache_template'); } if (!$cacheTemplate) { unlink($cacheFile); } return $val; }
public function testDefaultClasses() { Config::nest(); FormField::config()->update('default_classes', array('class1')); $field = new FormField('MyField'); $this->assertContains('class1', $field->extraClass(), 'Class list does not contain expected class'); FormField::config()->update('default_classes', array('class1', 'class2')); $field = new FormField('MyField'); $this->assertContains('class1 class2', $field->extraClass(), 'Class list does not contain expected class'); FormField::config()->update('default_classes', array('class3')); $field = new FormField('MyField'); $this->assertContains('class3', $field->extraClass(), 'Class list does not contain expected class'); $field->removeExtraClass('class3'); $this->assertNotContains('class3', $field->extraClass(), 'Class list contains unexpected class'); TextField::config()->update('default_classes', array('textfield-class')); $field = new TextField('MyField'); //check default classes inherit $this->assertContains('class3', $field->extraClass(), 'Class list does not contain inherited class'); $this->assertContains('textfield-class', $field->extraClass(), 'Class list does not contain expected class'); Config::unnest(); }
/** * Enable the default configuration of MySQL full-text searching on the given data classes. * It can be used to limit the searched classes, but not to add your own classes. * For this purpose, please use {@link Object::add_extension()} directly: * <code> * MyObject::add_extension("FulltextSearchable('MySearchableField,MyOtherField')"); * </code> * * Caution: This is a wrapper method that should only be used in _config.php, * and only be called once in your code. * * @param array $searchableClasses The extension will be applied to all DataObject subclasses * listed here. Default: {@link SiteTree} and {@link File}. * @throws Exception */ public static function enable($searchableClasses = array('SilverStripe\\CMS\\Model\\SiteTree', 'SilverStripe\\Assets\\File')) { $defaultColumns = array('SilverStripe\\CMS\\Model\\SiteTree' => '"Title","MenuTitle","Content","MetaDescription"', 'SilverStripe\\Assets\\File' => '"Name","Title"'); if (!is_array($searchableClasses)) { $searchableClasses = array($searchableClasses); } foreach ($searchableClasses as $class) { if (!class_exists($class)) { continue; } if (isset($defaultColumns[$class])) { Config::inst()->update($class, 'create_table_options', array(MySQLSchemaManager::ID => 'ENGINE=MyISAM')); $class::add_extension(__CLASS__ . "('{$defaultColumns[$class]}')"); } else { throw new Exception("FulltextSearchable::enable() I don't know the default search columns for class '{$class}'"); } } self::$searchable_classes = $searchableClasses; if (class_exists("SilverStripe\\CMS\\Controllers\\ContentController")) { ContentController::add_extension("SilverStripe\\CMS\\Search\\ContentControllerSearchExtension"); } }
/** * Test with default -v prefix */ public function testWithDefaultPrefix() { Config::inst()->update('SilverStripe\\Assets\\Storage\\DefaultAssetNameGenerator', 'version_prefix', '-v'); // Test with item that doesn't contain the prefix $generator = new DefaultAssetNameGenerator('folder/MyFile-001.jpg'); $suggestions = iterator_to_array($generator); $this->assertEquals(100, count($suggestions)); $this->assertEquals('folder/MyFile-001.jpg', $suggestions[0]); $this->assertEquals('folder/MyFile-001-v2.jpg', $suggestions[1]); $this->assertEquals('folder/MyFile-001-v4.jpg', $suggestions[3]); $this->assertEquals('folder/MyFile-001-v21.jpg', $suggestions[20]); $this->assertEquals('folder/MyFile-001-v99.jpg', $suggestions[98]); $this->assertNotEquals('folder/MyFile-001-v100.jpg', $suggestions[99]); // Last suggestion is semi-random // Test with item that contains prefix $generator = new DefaultAssetNameGenerator('folder/MyFile-v24.jpg'); $suggestions = iterator_to_array($generator); $this->assertEquals(100, count($suggestions)); $this->assertEquals('folder/MyFile-v24.jpg', $suggestions[0]); $this->assertEquals('folder/MyFile-v25.jpg', $suggestions[1]); $this->assertEquals('folder/MyFile-v26.jpg', $suggestions[2]); $this->assertEquals('folder/MyFile-v48.jpg', $suggestions[24]); $this->assertEquals('folder/MyFile-v122.jpg', $suggestions[98]); $this->assertNotEquals('folder/MyFile-v123.jpg', $suggestions[99]); $this->assertNotEquals('folder/MyFile-123.jpg', $suggestions[99]); // Test without numeric value $generator = new DefaultAssetNameGenerator('folder/MyFile.jpg'); $suggestions = iterator_to_array($generator); $this->assertEquals(100, count($suggestions)); $this->assertEquals('folder/MyFile.jpg', $suggestions[0]); $this->assertEquals('folder/MyFile-v2.jpg', $suggestions[1]); $this->assertEquals('folder/MyFile-v3.jpg', $suggestions[2]); $this->assertEquals('folder/MyFile-v25.jpg', $suggestions[24]); $this->assertEquals('folder/MyFile-v99.jpg', $suggestions[98]); $this->assertNotEquals('folder/MyFile-v100.jpg', $suggestions[99]); }
/** * this function is used to provide modifications to the fields labels in CMS * by the extension * By default, the fieldLabels() of its owner will merge more fields defined in the extension's * $extra_fields['field_labels'] * * @param array $labels Array of field labels */ public function updateFieldLabels(&$labels) { $field_labels = Config::inst()->get($this->class, 'field_labels'); if ($field_labels) { $labels = array_merge($labels, $field_labels); } }