diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index ab06260..97d08bf 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -239,6 +239,8 @@ const REGISTRY_WRITE_LOOKUP_CACHE = 2; */ const DRUPAL_PHP_FUNCTION_PATTERN = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; +require_once DRUPAL_ROOT . '/core/includes/config.inc'; + /** * Provides a caching wrapper to be used in place of large array structures. * @@ -766,7 +768,7 @@ function drupal_settings_initialize() { global $base_url, $base_path, $base_root; // Export the following settings.php variables to the global namespace - global $databases, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $db_prefix, $drupal_hash_salt, $is_https, $base_secure_url, $base_insecure_url; + global $databases, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $db_prefix, $drupal_hash_salt, $is_https, $base_secure_url, $base_insecure_url, $config_directory_name, $config_signature_key; $conf = array(); if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) { @@ -1342,8 +1344,10 @@ function drupal_page_header() { * response is sent. */ function drupal_serve_page_from_cache(stdClass $cache) { + $config = config('system.performance'); + // Negotiate whether to use compression. - $page_compression = variable_get('page_compression', TRUE) && extension_loaded('zlib'); + $page_compression = $config->get('page_compression') && extension_loaded('zlib'); $return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE; // Get headers set in hook_boot(). Keys are lower-case. @@ -1369,7 +1373,7 @@ function drupal_serve_page_from_cache(stdClass $cache) { // max-age > 0, allowing the page to be cached by external proxies, when a // session cookie is present unless the Vary header has been replaced or // unset in hook_boot(). - $max_age = !isset($_COOKIE[session_name()]) || isset($hook_boot_headers['vary']) ? variable_get('page_cache_maximum_age', 0) : 0; + $max_age = !isset($_COOKIE[session_name()]) || isset($hook_boot_headers['vary']) ? $config->get('page_cache_maximum_age') : 0; $default_headers['Cache-Control'] = 'public, max-age=' . $max_age; // Entity tag should change if the output changes. @@ -2335,7 +2339,8 @@ function _drupal_bootstrap_page_cache() { } else { drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES, FALSE); - $cache_enabled = variable_get('cache'); + $config = config('system.performance'); + $cache_enabled = $config->get('cache'); } drupal_block_denied(ip_address()); // If there is no session cookie and cache is enabled (or forced), try diff --git a/core/includes/common.inc b/core/includes/common.inc index 9c65647..3a79f0e 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -2597,7 +2597,8 @@ function drupal_page_footer() { // Commit the user session, if needed. drupal_session_commit(); - if (variable_get('cache', 0) && ($cache = drupal_page_set_cache())) { + $config = config('system.performance'); + if ($config->get('cache') && ($cache = drupal_page_set_cache())) { drupal_serve_page_from_cache($cache); } else { @@ -4405,7 +4406,7 @@ function drupal_group_js($javascript) { function drupal_aggregate_js(&$js_groups) { // Only aggregate when the site is configured to do so, and not during an // update. - if (variable_get('preprocess_js', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')) { + if (config('system.performance')->get('preprocess_js') && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')) { foreach ($js_groups as $key => $group) { if ($group['type'] == 'file' && $group['preprocess']) { $js_groups[$key]['data'] = drupal_build_js_cache($group['items']); @@ -5180,7 +5181,7 @@ function drupal_page_set_cache() { } if ($cache->data['body']) { - if (variable_get('page_compression', TRUE) && extension_loaded('zlib')) { + if (config('system.performance')->get('page_compression') && extension_loaded('zlib')) { $cache->data['body'] = gzencode($cache->data['body'], 9, FORCE_GZIP); } cache('page')->set($cache->cid, $cache->data, $cache->expire); diff --git a/core/includes/config.inc b/core/includes/config.inc new file mode 100644 index 0000000..c35eb97 --- /dev/null +++ b/core/includes/config.inc @@ -0,0 +1,254 @@ + $file) { + // Load config data into the active store and write it out to the + // file system in the drupal config directory. Note the config name + // needs to be the same as the file name WITHOUT the extension. + // @todo Make this acknowledge other storage engines rather than having + // SQL be hardcoded. + $parts = explode('/', $file); + $file = array_pop($parts); + $config_name = str_replace('.xml', '', $file); + + $verified_storage = new DrupalVerifiedStorageSQL($config_name); + $verified_storage->write(file_get_contents($module_config_dir . '/' . $file)); + } + } +} + +/** + * Retrieves an iterable array which lists the children under a config 'branch'. + * + * Given the following configuration files: + * - core.entity.node_type.article.xml + * - core.entity.node_type.page.xml + * + * You can pass a prefix 'core.entity.node_type' and get back an array of the + * filenames that match. This allows you to iterate through all files in a + * branch. Note that this will only work on the level above the tips, so + * a prefix of 'core.entity' would return an empty array. + * + * @param $prefix + * The prefix of the files we are searching for. + * + * @return + * An array of file names under a branch. + */ +function config_get_signed_file_storage_names_with_prefix($prefix = '') { + $files = glob(config_get_config_directory() . '/' . $prefix . '*.xml'); + $clean_name = function ($value) { + return basename($value, '.xml'); + }; + return array_map($clean_name, $files); +} + +/** + * Generates a hash of a config file's contents using our encryption key. + * + * @param $data + * The contents of a configuration file. + * + * @return + * A hash of the data. + */ +function config_sign_data($data) { + // The configuration key is loaded from settings.php and imported into the global namespace + global $config_signature_key; + + // SHA-512 is both secure and very fast on 64 bit CPUs. + // @todo What about 32-bit CPUs? + return hash_hmac('sha512', $data, $config_signature_key); +} + +/** + * @todo + * + * @param $prefix + * @todo + * + * @return + * @todo + */ +function config_get_verified_storage_names_with_prefix($prefix = '') { + return DrupalVerifiedStorageSQL::getNamesWithPrefix($prefix); +} + +/** + * @todo + * + * @param $prefix + * @todo + * + * @return + * @todo + */ +function config_get_names_with_prefix($prefix) { + return config_get_verified_storage_names_with_prefix($prefix); +} + +/** + * Retrieves a configuration object. + * + * This is the main entry point to the configuration API. Calling + * @code config(book.admin) @endcode will return a configuration object in which + * the book module can store its administrative settings. + * + * @param $name + * The name of the configuration object to retrieve. The name corresponds to + * an XML configuration file. For @code config(book.admin) @endcode, the + * config object returned will contain the contents of book.admin.xml. + * @param $class + * The class name of the config object to be returned. Defaults to + * DrupalConfig. + * + * @return + * An instance of the class specified in the $class parameter. + * + */ +function config($name, $class = 'Drupal\Core\Config\DrupalConfig') { + // @todo Replace this with an appropriate factory. + return new $class(new DrupalVerifiedStorageSQL($name)); +} + +/** + * Decodes configuration data from its native format to an associative array. + * + * @param $data + * Configuration data. + * + * @return + * An associative array representation of the data. + */ +function config_decode($data) { + if (empty($data)) { + return array(); + } + $xml = new SimpleXMLElement($data); + $json = json_encode($xml); + return json_decode($json, TRUE); +} + +/** + * Standardizes SimpleXML object output into simple arrays for easier use. + * + * @param $xmlObject + * A valid XML string. + * + * @return + * An array representation of A SimpleXML object. + */ +function config_xml_to_array($data) { + $out = array(); + $xmlObject = simplexml_load_string($data); + + if (is_object($xmlObject)) { + $attributes = (array) $xmlObject->attributes(); + if (isset($attributes['@attributes'])) { + $out['#attributes'] = $attributes['@attributes']; + } + } + if (trim((string) $xmlObject)) { + return trim((string) $xmlObject); + } + foreach ($xmlObject as $index => $content) { + if (is_object($content)) { + $out[$index] = config_xml_to_array($content); + } + } + + return $out; +} + +/** + * Encodes an array into the native configuration format. + * + * @param $data + * An associative array or an object + * + * @return + * A representation of this array or object in the native configuration + * format. + * + * @todo This needs to work for objects as well and currently doesn't. + */ +function config_encode($data) { + // creating object of SimpleXMLElement + $xml_object = new SimpleXMLElement(""); + + // function call to convert array to xml + config_array_to_xml($data, $xml_object); + + // Pretty print the result + $dom = new DOMDocument('1.0'); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($xml_object->asXML()); + // @todo The loaded XML can be invalid; throwing plenty of PHP warnings but no + // catchable error. + return $dom->saveXML(); +} + +/** + * Encodes an array into XML + * + * @param $data + * An associative array or an object + * + * @return + * A representation of this array or object in the native configuration + * format. + * + * @todo This needs to work for objects as well and currently doesn't. + */ +function config_array_to_xml($array, &$xml_object) { + foreach ($array as $key => $value) { + if (is_array($value)) { + if (!is_numeric($key)){ + $subnode = $xml_object->addChild("$key"); + config_array_to_xml($value, $subnode); + } + else { + config_array_to_xml($value, $xml_object); + } + } + else { + $xml_object->addChild($key, $value); + } + } +} diff --git a/core/includes/file.inc b/core/includes/file.inc index 05bf6c1..1fde2f4 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -467,6 +467,7 @@ function file_ensure_htaccess() { file_save_htaccess('private://', TRUE); } file_save_htaccess('temporary://', TRUE); + file_save_htaccess(config_get_config_directory(), TRUE); } /** diff --git a/core/includes/form.inc b/core/includes/form.inc index 85bffc6..1c3841f 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -856,7 +856,8 @@ function drupal_process_form($form_id, &$form, &$form_state) { // We'll clear out the cached copies of the form and its stored data // here, as we've finished with them. The in-memory copies are still // here, though. - if (!variable_get('cache', 0) && !empty($form_state['values']['form_build_id'])) { + $config = config('system.performance'); + if (!$config->get('cache') && !empty($form_state['values']['form_build_id'])) { cache('form')->delete('form_' . $form_state['values']['form_build_id']); cache('form')->delete('form_state_' . $form_state['values']['form_build_id']); } diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index c21cd68..f0b969f 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -986,7 +986,36 @@ function install_settings_form_submit($form, &$form_state) { 'value' => drupal_hash_base64(drupal_random_bytes(55)), 'required' => TRUE, ); + + $settings['config_signature_key'] = array( + 'value' => drupal_hash_base64(drupal_random_bytes(55)), + 'required' => TRUE, + ); + + // This duplicates drupal_get_token() because that function can't work yet. + // Wondering if it makes sense to move this later in the process, but its + // nice having all the settings stuff here. + // + // @todo This is actually causing a bug right now, because you can install + // without hitting install_settings_form_submit() if your settings.php + // already has the db stuff in it, and right now in that case your + // config directory never gets created. So this needs to be moved elsewhere. + $settings['config_directory_name'] = array( + 'value' => 'config_' . drupal_hmac_base64('', session_id() . $settings['config_signature_key']['value'] . $settings['drupal_hash_salt']['value']), + 'required' => TRUE, + ); + drupal_rewrite_settings($settings); + // Actually create the config directory named above. + $config_path = conf_path() . '/files/' . $settings['config_directory_name']['value']; + if (!file_prepare_directory($config_path, FILE_CREATE_DIRECTORY)) { + // How best to handle errors here? + }; + + // Write out a .htaccess file that will protect the config directory from + // prying eyes. + file_save_htaccess($config_path, TRUE); + // Indicate that the settings file has been verified, and check the database // for the last completed task, now that we have a valid connection. This // last step is important since we want to trigger an error if the new diff --git a/core/includes/install.inc b/core/includes/install.inc index 533678f..a7b4dc7 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -730,6 +730,7 @@ function drupal_install_system() { )) ->execute(); system_rebuild_module_data(); + config_install_default_config('system'); } /** diff --git a/core/includes/language.inc b/core/includes/language.inc index 4628053..d1cc818 100644 --- a/core/includes/language.inc +++ b/core/includes/language.inc @@ -343,7 +343,8 @@ function language_provider_invoke($provider_id, $provider = NULL) { // If the language provider has no cache preference or this is satisfied // we can execute the callback. - $cache = !isset($provider['cache']) || $user->uid || $provider['cache'] == variable_get('cache', 0); + $config = config('system.performance'); + $cache = !isset($provider['cache']) || $user->uid || $provider['cache'] == $config->get('cache'); $callback = isset($provider['callbacks']['language']) ? $provider['callbacks']['language'] : FALSE; $langcode = $cache && function_exists($callback) ? $callback($languages) : FALSE; $results[$provider_id] = isset($languages[$langcode]) ? $languages[$langcode] : FALSE; diff --git a/core/includes/module.inc b/core/includes/module.inc index d61aba9..aa9eaf5 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -461,6 +461,9 @@ function module_enable($module_list, $enable_dependencies = TRUE) { $versions = drupal_get_schema_versions($module); $version = $versions ? max($versions) : SCHEMA_INSTALLED; + // Copy any default configuration data to the system config directory/ + config_install_default_config($module); + // If the module has no current updates, but has some that were // previously removed, set the version to the value of // hook_update_last_removed(). diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php index 37af13c..7afd698 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -101,7 +101,8 @@ class DatabaseBackend implements CacheBackendInterface { // timer. The cache variable is loaded into the $user object by // _drupal_session_read() in session.inc. If the data is permanent or we're // not enforcing a minimum cache lifetime always return the cached data. - if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && $user->cache > $cache->created) { + $config = config('system.performance'); + if ($cache->expire != CACHE_PERMANENT && $config->get('cache_lifetime') && $user->cache > $cache->created) { // This cache data is too old and thus not valid for us, ignore it. return FALSE; } diff --git a/core/lib/Drupal/Core/Config/ConfigException.php b/core/lib/Drupal/Core/Config/ConfigException.php new file mode 100644 index 0000000..c60a449 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigException.php @@ -0,0 +1,8 @@ +_verifiedStorage = $verified_storage; + $this->read(); + } + + /** + * Reads config data from the active store into our object. + */ + public function read() { + $active = (array) config_decode($this->_verifiedStorage->read()); + foreach ($active as $key => $value) { + $this->set($key, $value); + } + } + + /** + * Checks whether a particular value is overridden. + * + * @param $key + * @todo + * + * @return + * @todo + */ + public function isOverridden($key) { + return isset($this->_overrides[$key]); + } + + /** + * Gets data from this config object. + * + * @param $key + * A string that maps to a key within the configuration data. + * For instance in the following XML: + * @code + * + * baz + * + * @endcode + * A key of 'foo.bar' would return the string 'baz'. However, a key of 'foo' + * would return array('bar' => 'baz'). + * If no key is specified, then the entire data array is returned. + * + * The configuration system does not retain data types. Every saved value is + * casted to a string. In most cases this is not an issue; however, it can + * cause issues with Booleans, which are casted to "1" (TRUE) or "0" (FALSE). + * In particular, code relying on === or !== will no longer function properly. + * + * @see http://php.net/manual/en/language.operators.comparison.php. + * + * @return + * The data that was requested. + */ + public function get($key = '') { + if (empty($key)) { + return $this->data; + } + else { + $parts = explode('.', $key); + if (count($parts) == 1) { + return isset($this->data[$key]) ? $this->data[$key] : NULL; + } + else { + $key_exists = NULL; + $value = drupal_array_get_nested_value($this->data, $parts, $key_exists); + return $key_exists ? $value : NULL; + } + } + } + + /** + * Sets value in this config object. + * + * @param $key + * @todo + * @param $value + * @todo + */ + public function set($key, $value) { + // Remove all non-alphanumeric characters from the key. + // @todo Reverse this and throw an exception when encountering a key with + // invalid name. The identical validation also needs to happen in get(). + // Furthermore, the dot/period is a reserved character; it may appear + // between keys, but not within keys. + $key = preg_replace('@[^a-zA-Z0-9_.-]@', '', $key); + + // Type-cast value into a string. + $value = $this->castValue($value); + + $parts = explode('.', $key); + if (count($parts) == 1) { + $this->data[$key] = $value; + } + else { + drupal_array_set_nested_value($this->data, $parts, $value); + } + return $this; + } + + /** + * Casts a saved value to a string. + * + * The configuration system only saves strings or arrays. Any scalar + * non-string value is cast to a string. The one exception is boolean FALSE + * which would normally become '' when cast to a string, but is manually + * cast to '0' here for convenience and consistency. + * + * Any non-scalar value that is not an array (aka objects) gets cast + * to an array. + * + * @param $value + * A value being saved into the configuration system. + * @param $value + * The value cast to a string or array. + */ + public function castValue($value) { + if (is_scalar($value)) { + // Handle special case of FALSE, which should be '0' instead of ''. + if ($value === FALSE) { + $value = '0'; + } + else { + $value = (string) $value; + } + } + else { + // Any non-scalar value must be an array. + if (!is_array($value)) { + $value = (array) $value; + } + // Recurse into any nested keys. + foreach ($value as $key => $nested_value) { + $value[$key] = $this->castValue($nested_value); + } + } + return $value; + } + + /** + * Unsets value in this config object. + * + * @param $key + * Name of the key whose value should be unset. + */ + public function clear($key) { + $parts = explode('.', $key); + if (count($parts) == 1) { + unset($this->data[$key]); + } + else { + drupal_array_unset_nested_value($this->data, $parts); + } + } + + /** + * Saves the configuration object to disk as XML. + */ + public function save() { + $this->_verifiedStorage->write(config_encode($this->data)); + } + + /** + * Deletes the configuration object on disk. + */ + public function delete() { + $this->data = array(); + $this->_verifiedStorage->delete(); + } +} diff --git a/core/lib/Drupal/Core/Config/DrupalConfigVerifiedStorage.php b/core/lib/Drupal/Core/Config/DrupalConfigVerifiedStorage.php new file mode 100644 index 0000000..ca37cdd --- /dev/null +++ b/core/lib/Drupal/Core/Config/DrupalConfigVerifiedStorage.php @@ -0,0 +1,102 @@ +name = $name; + } + + /** + * Instantiates a new signed file object or returns the existing one. + * + * @return SignedFileStorage + * The signed file object for this configuration object. + */ + protected function signedFileStorage() { + if (!isset($this->signedFile)) { + $this->signedFile = new SignedFileStorage($this->name); + } + return $this->signedFile; + } + + /** + * Implements DrupalConfigVerifiedStorageInterface::copyToFile(). + */ + public function copyToFile() { + return $this->writeToFile($this->read()); + } + + /** + * Implements DrupalConfigVerifiedStorageInterface::deleteFile(). + */ + public function deleteFile() { + return $this->signedFileStorage()->delete(); + } + + /** + * Implements DrupalConfigVerifiedStorageInterface::copyFromFile(). + */ + public function copyFromFile() { + return $this->writeToActive($this->readFromFile()); + } + + /** + * @todo + * + * @return + * @todo + */ + public function readFromFile() { + return $this->signedFileStorage()->read($this->name); + } + + /** + * Implements DrupalConfigVerifiedStorageInterface::isOutOfSync(). + */ + public function isOutOfSync() { + return $this->read() !== $this->readFromFile(); + } + + /** + * Implements DrupalConfigVerifiedStorageInterface::write(). + */ + public function write($data) { + $this->writeToActive($data); + $this->writeToFile($data); + } + + /** + * Implements DrupalConfigVerifiedStorageInterface::writeToFile(). + */ + public function writeToFile($data) { + return $this->signedFileStorage()->write($data); + } + + /** + * Implements DrupalConfigVerifiedStorageInterface::delete(). + */ + public function delete() { + $this->deleteFromActive(); + $this->deleteFile(); + } +} diff --git a/core/lib/Drupal/Core/Config/DrupalConfigVerifiedStorageInterface.php b/core/lib/Drupal/Core/Config/DrupalConfigVerifiedStorageInterface.php new file mode 100644 index 0000000..2fdce76 --- /dev/null +++ b/core/lib/Drupal/Core/Config/DrupalConfigVerifiedStorageInterface.php @@ -0,0 +1,84 @@ + $this->name))->fetchField(); + } + } + + /** + * Implements DrupalConfigVerifiedStorageInterface::writeToActive(). + */ + public function writeToActive($data) { + return db_merge('config') + ->key(array('name' => $this->name)) + ->fields(array('data' => $data)) + ->execute(); + } + + /** + * @todo + */ + public function deleteFromActive() { + db_delete('config') + ->condition('name', $this->name) + ->execute(); + } + + /** + * Implements DrupalConfigVerifiedStorageInterface::getNamesWithPrefix(). + */ + static public function getNamesWithPrefix($prefix = '') { + return db_query('SELECT name FROM {config} WHERE name LIKE :name', array(':name' => db_like($prefix) . '%'))->fetchCol(); + } +} diff --git a/core/lib/Drupal/Core/Config/SignedFileStorage.php b/core/lib/Drupal/Core/Config/SignedFileStorage.php new file mode 100644 index 0000000..1e23d74 --- /dev/null +++ b/core/lib/Drupal/Core/Config/SignedFileStorage.php @@ -0,0 +1,145 @@ +name = $name; + } + + /** + * Reads and returns a signed file and its signature. + * + * @return + * An array with "signature" and "data" keys. + * + * @throws + * Exception + */ + protected function readWithSignature() { + $content = file_get_contents($this->getFilePath()); + if ($content === FALSE) { + throw new \Exception('Read file is invalid.'); + } + $signature = file_get_contents($this->getFilePath() . '.sig'); + if ($signature === FALSE) { + throw new \Exception('Signature file is invalid.'); + } + return array('data' => $content, 'signature' => $signature); + } + + /** + * Checks whether the XML configuration file already exists on disk. + * + * @return + * @todo + */ + protected function exists() { + return file_exists($this->getFilePath()); + } + + /** + * Returns the path to the XML configuration file. + * + * @return + * @todo + */ + public function getFilePath() { + return config_get_config_directory() . '/' . $this->name . '.xml'; + } + + /** + * Recreates the signature for the file. + */ + public function resign() { + if ($this->exists()) { + $parts = $this->readWithSignature(); + $this->write($parts['data']); + } + } + + /** + * Cryptographically verifies the integrity of the configuration file. + * + * @param $contentOnSuccess + * Whether or not to return the contents of the verified configuration file. + * + * @return mixed + * If $contentOnSuccess was TRUE, returns the contents of the verified + * configuration file; otherwise returns TRUE on success. Always returns + * FALSE if the configuration file was not successfully verified. + */ + public function verify($contentOnSuccess = FALSE) { + if ($this->exists()) { + $split = $this->readWithSignature(); + $expected_signature = config_sign_data($split['data']); + if ($expected_signature === $split['signature']) { + if ($contentOnSuccess) { + return $split['data']; + } + return TRUE; + } + } + return FALSE; + } + + /** + * Writes the contents of the configuration file to disk. + * + * @param $data + * The data to be written to the file. + * + * @throws + * Exception + * + * @todo What format is $data in? + */ + public function write($data) { + $signature = config_sign_data($data); + if (!file_put_contents($this->getFilePath(), $data)) { + throw new \Exception('Failed to write configuration file: ' . $this->getFilePath()); + } + if (!file_put_contents($this->getFilePath() . '.sig', $signature)) { + throw new \Exception('Failed to write signature file: ' . $this->getFilePath()); + } + } + + /** + * Returns the contents of the configuration file. + * + * @return + * @todo + */ + public function read() { + if ($this->exists()) { + $verification = $this->verify(TRUE); + if ($verification === FALSE) { + throw new \Exception('Invalid signature in file header.'); + } + return $verification; + } + } + + /** + * Deletes a configuration file. + */ + public function delete() { + // Needs error handling and etc. + @drupal_unlink($this->getFilePath()); + @drupal_unlink($this->getFilePath() . '.sig'); + } +} diff --git a/core/modules/block/config/block.performance.xml b/core/modules/block/config/block.performance.xml new file mode 100644 index 0000000..ff3f7e1 --- /dev/null +++ b/core/modules/block/config/block.performance.xml @@ -0,0 +1,4 @@ + + + 0 + diff --git a/core/modules/color/color.test b/core/modules/color/color.test index 06e4cb9..616d19a 100644 --- a/core/modules/color/color.test +++ b/core/modules/color/color.test @@ -92,7 +92,9 @@ class ColorTestCase extends DrupalWebTestCase { $this->assertTrue(strpos($stylesheet_content, 'color: ' . $test_values['scheme_color']) !== FALSE, 'Make sure the color we changed is in the color stylesheet. (' . $theme . ')'); // Test with aggregated CSS turned on. - variable_set('preprocess_css', 1); + $config = config('system.performance'); + $config->set('preprocess_css', 1); + $config->save(); $this->drupalGet(''); $stylesheets = variable_get('drupal_css_cache_files', array()); $stylesheet_content = ''; @@ -100,7 +102,8 @@ class ColorTestCase extends DrupalWebTestCase { $stylesheet_content .= join("\n", file(drupal_realpath($uri))); } $this->assertTrue(strpos($stylesheet_content, 'public://') === FALSE, 'Make sure the color paths have been translated to local paths. (' . $theme . ')'); - variable_set('preprocess_css', 0); + $config->set('preprocess_css', 0); + $config->save(); } /** diff --git a/core/modules/config/config.info b/core/modules/config/config.info new file mode 100644 index 0000000..643a3fc --- /dev/null +++ b/core/modules/config/config.info @@ -0,0 +1,5 @@ +name = Configuration manager +package = Core +version = VERSION +core = 8.x +files[] = config.test diff --git a/core/modules/config/config.module b/core/modules/config/config.module new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/core/modules/config/config.module @@ -0,0 +1 @@ + 'File security', + 'description' => 'Tests security of saved configuration files.', + 'group' => 'Configuration', + ); + } + + /** + * Tests that a file written by this system has a valid signature. + */ + function testFileVerify() { + $file = new SignedFileStorage($this->filename); + $file->write($this->testContent); + + $this->assertTrue($file->verify(), 'A file verifies after being written.'); + + unset($file); + + // Load the file again, so that there is no stale data from the old object. + $file = new SignedFileStorage($this->filename); + $this->assertTrue($file->verify(), 'A file verifies after being written and reloaded.'); + } + + /** + * Tests that a file written by this system can be successfully read back. + */ + function testFilePersist() { + $file = new SignedFileStorage($this->filename); + $file->write($this->testContent); + + unset($file); + + // Reading should throw an exception in case of bad validation. + // Note that if any other exception is thrown, we let the test system + // handle catching and reporting it. + try { + $file = new SignedFileStorage($this->filename); + $saved_content = $file->read(); + + $this->assertEqual($saved_content, $this->testContent, 'A file can be read back successfully.'); + } + catch (Exception $e) { + $this->fail('File failed verification when being read.'); + } + } + + /** + * Tests that a file fails validation if it's been monkeyed with. + */ + function testFileNotVerify() { + $file = new SignedFileStorage($this->filename); + $file->write($this->testContent); + + // Manually overwrite the body of the secure file. Note that we skip the + // first line, which is reserved for the signature and such, to overwrite + // just the payload. + $raw_file = new SplFileObject($file->getFilePath(), 'a+'); + $raw_file->fwrite('Good morning, Detroit!'); + $raw_file->fflush(); + unset($raw_file); + + unset($file); + + $file = new SignedFileStorage($this->filename); + $this->assertFalse($file->verify(), 'Corrupted file does not verify.'); + } +} + +/** + * Tests reading and writing file contents. + */ +class ConfigFileContentTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + + protected $fileExtension = 'xml'; + + public static function getInfo() { + return array( + 'name' => 'File content', + 'description' => 'Tests reading and writing of configuration files.', + 'group' => 'Configuration', + ); + } + + /** + * Tests setting, writing, and reading of a configuration setting. + */ + function testReadWriteConfig() { + $config_dir = config_get_config_directory(); + $name = 'foo.bar'; + $key = 'foo'; + $value = 'bar'; + $nested_key = 'biff.bang'; + $nested_value = 'pow'; + $array_key = 'array'; + $array_value = array( + 'foo' => 'bar', + 'biff' => array( + 'bang' => 'pow', + ) + ); + $nested_array_key = 'nested.array'; + $true_key = 'true'; + $false_key = 'false'; + + // Attempt to read non-existing configuration. + $config = config($name); + + // Verify an configuration object is returned. +// $this->assertEqual($config->name, $name); + $this->assertTrue($config, t('Config object created.')); + + // Verify the configuration object is empty. + $this->assertEqual($config->get(), array(), t('New config object is empty.')); + + // Verify nothing was saved. + $db_config = db_query('SELECT * FROM {config} WHERE name = :name', array(':name' => $name))->fetch(); + $this->assertIdentical($db_config, FALSE, t('Active store does not have a record for %name', array('%name' => $name))); + $this->assertFalse(file_exists($config_dir . '/' . $name . '.' . $this->fileExtension), 'Configuration file does not exist.'); + + // Add a top level value + $config = config($name); + $config->set($key, $value); + + // Add a nested value + $config->set($nested_key, $nested_value); + + // Add an array + $config->set($array_key, $array_value); + + // Add a nested array + $config->set($nested_array_key, $array_value); + + // Add a boolean false value. Should get cast to 0 + $config->set($false_key, FALSE); + + // Add a boolean true value. Should get cast to 1 + $config->set($true_key, TRUE); + $config->save(); + + // Verify the database entry exists. + $db_config = db_query('SELECT * FROM {config} WHERE name = :name', array(':name' => $name))->fetch(); + $this->assertEqual($db_config->name, $name, t('After saving configuration, active store has a record for %name', array('%name' => $name))); + + // Verify the file exists. + $this->assertTrue(file_exists($config_dir . '/' . $name . '.' . $this->fileExtension), t('After saving configuration, config file exists.')); + + // Read top level value + $config = config($name); +// $this->assertEqual($config->name, $name); + $this->assertTrue($config, 'Config object created.'); + $this->assertEqual($config->get($key), 'bar', t('Top level configuration value found.')); + + // Read nested value + $this->assertEqual($config->get($nested_key), $nested_value, t('Nested configuration value found.')); + + // Read array + $this->assertEqual($config->get($array_key), $array_value, t('Top level array configuration value found.')); + + // Read nested array + $this->assertEqual($config->get($nested_array_key), $array_value, t('Nested array configuration value found.')); + + // Read a top level value that doesn't exist + $this->assertNull($config->get('i_dont_exist'), t('Non-existent top level value returned NULL.')); + + // Read a nested value that doesn't exist + $this->assertNull($config->get('i.dont.exist'), t('Non-existent nested value returned NULL.')); + + // Read false value + $this->assertEqual($config->get($false_key), '0', t('Boolean FALSE value returned the string \'0\'.')); + + // Read true value + $this->assertEqual($config->get($true_key), '1', t('Boolean TRUE value returned the string \'1\'.')); + + // Unset a top level value + $config->clear($key); + + // Unset a nested value + $config->clear($nested_key); + $config->save(); + $config = config($name); + + // Read unset top level value + $this->assertNull($config->get($key), t('Top level value unset.')); + + // Read unset nested value + $this->assertNull($config->get($nested_key), t('Nested value unset.')); + + // Delete the configuration. + $config = config($name); + $config->delete(); + + // Verify the database entry no longer exists. + $db_config = db_query('SELECT * FROM {config} WHERE name = :name', array(':name' => $name))->fetch(); + $this->assertIdentical($db_config, FALSE); + $this->assertFalse(file_exists($config_dir . '/' . $name . '.' . $this->fileExtension)); + + // Chainable ->set()->save() + // Attempt to delete non-existing configuration. + // Type casting into string. (recursively) + // List config names by prefix. (and without prefix) + } +} diff --git a/core/modules/image/config/image.styles.large.xml b/core/modules/image/config/image.styles.large.xml new file mode 100644 index 0000000..62448c1 --- /dev/null +++ b/core/modules/image/config/image.styles.large.xml @@ -0,0 +1,16 @@ + + + large + + + image_scale + image_scale_480_480_1 + + 480 + 480 + 1 + + 0 + + + diff --git a/core/modules/image/config/image.styles.medium.xml b/core/modules/image/config/image.styles.medium.xml new file mode 100644 index 0000000..d301877 --- /dev/null +++ b/core/modules/image/config/image.styles.medium.xml @@ -0,0 +1,16 @@ + + + medium + + + image_scale + image_scale_220_220_1 + + 220 + 220 + 1 + + 0 + + + diff --git a/core/modules/image/config/image.styles.thumbnail.xml b/core/modules/image/config/image.styles.thumbnail.xml new file mode 100644 index 0000000..385abe6 --- /dev/null +++ b/core/modules/image/config/image.styles.thumbnail.xml @@ -0,0 +1,16 @@ + + + thumbnail + + + image_scale + image_scale_100_100_1 + + 100 + 100 + 1 + + 0 + + + diff --git a/core/modules/image/image.admin.inc b/core/modules/image/image.admin.inc index 9643841..539e756 100644 --- a/core/modules/image/image.admin.inc +++ b/core/modules/image/image.admin.inc @@ -38,13 +38,6 @@ function image_style_form($form, &$form_state, $style) { $title = t('Edit %name style', array('%name' => $style['name'])); drupal_set_title($title, PASS_THROUGH); - // Adjust this form for styles that must be overridden to edit. - $editable = (bool) ($style['storage'] & IMAGE_STORAGE_EDITABLE); - - if (!$editable && empty($form_state['input'])) { - drupal_set_message(t('This image style is currently being provided by a module. Click the "Override defaults" button to change its settings.'), 'warning'); - } - $form_state['image_style'] = $style; $form['#tree'] = TRUE; $form['#attached']['css'][drupal_get_path('module', 'image') . '/image.admin.css'] = array(); @@ -56,27 +49,15 @@ function image_style_form($form, &$form_state, $style) { '#markup' => theme('image_style_preview', array('style' => $style)), ); - // Allow the name of the style to be changed, unless this style is - // provided by a module's hook_default_image_styles(). - if ($style['storage'] & IMAGE_STORAGE_MODULE) { - $form['name'] = array( - '#type' => 'item', - '#title' => t('Image style name'), - '#markup' => $style['name'], - '#description' => t('This image style is being provided by %module module and may not be renamed.', array('%module' => $style['module'])), - ); - } - else { - $form['name'] = array( - '#type' => 'textfield', - '#size' => '64', - '#title' => t('Image style name'), - '#default_value' => $style['name'], - '#description' => t('The name is used in URLs for generated images. Use only lowercase alphanumeric characters, underscores (_), and hyphens (-).'), - '#element_validate' => array('image_style_name_validate'), - '#required' => TRUE, - ); - } + $form['name'] = array( + '#type' => 'textfield', + '#size' => '64', + '#title' => t('Image style name'), + '#default_value' => $style['name'], + '#description' => t('The name is used in URLs for generated images. Use only lowercase alphanumeric characters, underscores (_), and hyphens (-).'), + '#element_validate' => array('image_style_name_validate'), + '#required' => TRUE, + ); // Build the list of existing image effects for this image style. $form['effects'] = array( @@ -95,25 +76,19 @@ function image_style_form($form, &$form_state, $style) { '#title' => t('Weight for @title', array('@title' => $effect['label'])), '#title_display' => 'invisible', '#default_value' => $effect['weight'], - '#access' => $editable, ); - // Only attempt to display these fields for editable styles as the 'ieid' - // key is not set for styles defined in code. - if ($editable) { - $form['effects'][$key]['configure'] = array( - '#type' => 'link', - '#title' => t('edit'), - '#href' => 'admin/config/media/image-styles/edit/' . $style['name'] . '/effects/' . $effect['ieid'], - '#access' => $editable && isset($effect['form callback']), - ); - $form['effects'][$key]['remove'] = array( - '#type' => 'link', - '#title' => t('delete'), - '#href' => 'admin/config/media/image-styles/edit/' . $style['name'] . '/effects/' . $effect['ieid'] . '/delete', - '#access' => $editable, - ); - } + $form['effects'][$key]['configure'] = array( + '#type' => 'link', + '#title' => t('edit'), + '#href' => 'admin/config/media/image-styles/edit/' . $style['name'] . '/effects/' . $key, + '#access' => isset($effect['form callback']), + ); + $form['effects'][$key]['remove'] = array( + '#type' => 'link', + '#title' => t('delete'), + '#href' => 'admin/config/media/image-styles/edit/' . $style['name'] . '/effects/' . $key . '/delete', + ); } // Build the new image effect addition form and add it to the effect list. @@ -124,7 +99,6 @@ function image_style_form($form, &$form_state, $style) { $form['effects']['new'] = array( '#tree' => FALSE, '#weight' => isset($form_state['input']['weight']) ? $form_state['input']['weight'] : NULL, - '#access' => $editable, ); $form['effects']['new']['new'] = array( '#type' => 'select', @@ -148,17 +122,9 @@ function image_style_form($form, &$form_state, $style) { // Show the Override or Submit button for this style. $form['actions'] = array('#type' => 'actions'); - $form['actions']['override'] = array( - '#type' => 'submit', - '#value' => t('Override defaults'), - '#validate' => array(), - '#submit' => array('image_style_form_override_submit'), - '#access' => !$editable, - ); $form['actions']['submit'] = array( '#type' => 'submit', '#value' => t('Update style'), - '#access' => $editable, ); return $form; @@ -188,43 +154,54 @@ function image_style_form_add_submit($form, &$form_state) { } // If there's no form, immediately add the image effect. else { - $effect['isid'] = $style['isid']; - $effect['weight'] = $form_state['values']['weight']; - image_effect_save($effect); + $effect = array( + 'name' => $effect['name'], + 'data' => array(), + 'weight' => $form_state['values']['weight'], + ); + image_effect_save($style['name'], $effect); drupal_set_message(t('The image effect was successfully applied.')); } } /** - * Submit handler for overriding a module-defined style. - */ -function image_style_form_override_submit($form, &$form_state) { - drupal_set_message(t('The %style style has been overridden, allowing you to change its settings.', array('%style' => $form_state['image_style']['name']))); - image_default_style_save($form_state['image_style']); -} - -/** * Submit handler for saving an image style. */ function image_style_form_submit($form, &$form_state) { - // Update the image style name if it has changed. $style = $form_state['image_style']; - if (isset($form_state['values']['name']) && $style['name'] != $form_state['values']['name']) { - $style['name'] = $form_state['values']['name']; - } // Update image effect weights. if (!empty($form_state['values']['effects'])) { foreach ($form_state['values']['effects'] as $ieid => $effect_data) { if (isset($style['effects'][$ieid])) { - $effect = $style['effects'][$ieid]; - $effect['weight'] = $effect_data['weight']; - image_effect_save($effect); + $effect = array( + 'name' => $style['effects'][$ieid]['name'], + 'data' => $style['effects'][$ieid]['data'], + 'weight' => $effect_data['weight'], + 'ieid' => $ieid, + ); + $style['effects'][$ieid] = $effect; } } } + // Update the image style name if it has changed. We also need to delete the + // old style, because there is no concept of rename at the moment, just + // create and delete. + // @todo The config API does not yet support the concept of a rename, but + // hooks need to be able to change old style name references to the new + // name, so first save the new style, then delete the old, so that the + // delete function can receive the name of a fully saved style to update + // references to. + if (isset($form_state['values']['name']) && $style['name'] != $form_state['values']['name']) { + $old_style = $style; + $style['name'] = $form_state['values']['name']; + } image_style_save($style); + if (isset($old_style)) { + image_style_delete($old_style, $style['name']); + } + if ($form_state['values']['op'] == t('Update style')) { drupal_set_message(t('Changes to the style have been saved.')); } @@ -273,7 +250,7 @@ function image_style_add_form_submit($form, &$form_state) { function image_style_name_validate($element, $form_state) { // Check for duplicates. $styles = image_styles(); - if (isset($styles[$element['#value']]) && (!isset($form_state['image_style']['isid']) || $styles[$element['#value']]['isid'] != $form_state['image_style']['isid'])) { + if (isset($styles[$element['#value']]) && (!isset($form_state['image_style']['name']) || $styles[$element['#value']]['name'] != $form_state['image_style']['name'])) { form_set_error($element['#name'], t('The image style name %name is already in use.', array('%name' => $element['#value']))); } @@ -324,30 +301,6 @@ function image_style_delete_form_submit($form, &$form_state) { } /** - * Confirmation form to revert a database style to its default. - */ -function image_style_revert_form($form, $form_state, $style) { - $form_state['image_style'] = $style; - - return confirm_form( - $form, - t('Revert the %style style?', array('%style' => $style['name'])), - 'admin/config/media/image-styles', - t('Reverting this style will delete the customized settings and restore the defaults provided by the @module module.', array('@module' => $style['module'])), - t('Revert'), t('Cancel') - ); -} - -/** - * Submit handler to convert an overridden style to its default. - */ -function image_style_revert_form_submit($form, &$form_state) { - drupal_set_message(t('The %style style has been reverted to its defaults.', array('%style' => $form_state['image_style']['name']))); - image_default_style_revert($form_state['image_style']); - $form_state['redirect'] = 'admin/config/media/image-styles'; -} - -/** * Form builder; Form for adding and editing image effects. * * This form is used universally for editing all image effects. Each effect adds @@ -416,12 +369,14 @@ function image_effect_form($form, &$form_state, $style, $effect) { * Submit handler for updating an image effect. */ function image_effect_form_submit($form, &$form_state) { - $style = $form_state['image_style']; - $effect = array_merge($form_state['image_effect'], $form_state['values']); - $effect['isid'] = $style['isid']; - image_effect_save($effect); + $effect = array( + 'name' => $form_state['image_effect']['name'], + 'data' => $form_state['values']['data'], + 'weight' => $form_state['values']['weight'], + ); + image_effect_save($form_state['image_style']['name'], $effect); drupal_set_message(t('The image effect was successfully applied.')); - $form_state['redirect'] = 'admin/config/media/image-styles/edit/' . $style['name']; + $form_state['redirect'] = 'admin/config/media/image-styles/edit/' . $form_state['image_style']['name']; } /** @@ -449,7 +404,7 @@ function image_effect_delete_form_submit($form, &$form_state) { $style = $form_state['image_style']; $effect = $form_state['image_effect']; - image_effect_delete($effect); + image_effect_delete($style['name'], $effect); drupal_set_message(t('The image effect %name has been deleted.', array('%name' => $effect['label']))); $form_state['redirect'] = 'admin/config/media/image-styles/edit/' . $style['name']; } @@ -645,31 +600,19 @@ function image_rotate_form($data) { function theme_image_style_list($variables) { $styles = $variables['styles']; - $header = array(t('Style name'), t('Settings'), array('data' => t('Operations'), 'colspan' => 3)); + $header = array(t('Style name'), array('data' => t('Operations'), 'colspan' => 3)); $rows = array(); + $link_attributes = array( + 'attributes' => array( + 'class' => array('image-style-link'), + ), + ); + foreach ($styles as $style) { $row = array(); $row[] = l($style['name'], 'admin/config/media/image-styles/edit/' . $style['name']); - $link_attributes = array( - 'attributes' => array( - 'class' => array('image-style-link'), - ), - ); - if ($style['storage'] == IMAGE_STORAGE_NORMAL) { - $row[] = t('Custom'); - $row[] = l(t('edit'), 'admin/config/media/image-styles/edit/' . $style['name'], $link_attributes); - $row[] = l(t('delete'), 'admin/config/media/image-styles/delete/' . $style['name'], $link_attributes); - } - elseif ($style['storage'] == IMAGE_STORAGE_OVERRIDE) { - $row[] = t('Overridden'); - $row[] = l(t('edit'), 'admin/config/media/image-styles/edit/' . $style['name'], $link_attributes); - $row[] = l(t('revert'), 'admin/config/media/image-styles/revert/' . $style['name'], $link_attributes); - } - else { - $row[] = t('Default'); - $row[] = l(t('edit'), 'admin/config/media/image-styles/edit/' . $style['name'], $link_attributes); - $row[] = ''; - } + $row[] = l(t('edit'), 'admin/config/media/image-styles/edit/' . $style['name'], $link_attributes); + $row[] = l(t('delete'), 'admin/config/media/image-styles/delete/' . $style['name'], $link_attributes); $rows[] = $row; } @@ -694,13 +637,12 @@ function theme_image_style_list($variables) { */ function theme_image_style_effects($variables) { $form = $variables['form']; - $rows = array(); foreach (element_children($form) as $key) { $row = array(); $form[$key]['weight']['#attributes']['class'] = array('image-effect-order-weight'); - if (is_numeric($key)) { + if ($key != 'new') { $summary = drupal_render($form[$key]['summary']); $row[] = drupal_render($form[$key]['label']) . (empty($summary) ? '' : ' ' . $summary); $row[] = drupal_render($form[$key]['weight']); @@ -728,7 +670,7 @@ function theme_image_style_effects($variables) { array('data' => t('Operations'), 'colspan' => 2), ); - if (count($rows) == 1 && $form['new']['#access']) { + if (count($rows) == 1 && (!isset($form['new']['#access']) || $form['new']['#access'])) { array_unshift($rows, array(array( 'data' => t('There are currently no effects in this style. Add one by selecting an option below.'), 'colspan' => 4, diff --git a/core/modules/image/image.install b/core/modules/image/image.install index 02be57c..91349a9 100644 --- a/core/modules/image/image.install +++ b/core/modules/image/image.install @@ -23,87 +23,6 @@ function image_uninstall() { } /** - * Implements hook_schema(). - */ -function image_schema() { - $schema = array(); - - $schema['image_styles'] = array( - 'description' => 'Stores configuration options for image styles.', - 'fields' => array( - 'isid' => array( - 'description' => 'The primary identifier for an image style.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'name' => array( - 'description' => 'The style name.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - ), - ), - 'primary key' => array('isid'), - 'unique keys' => array( - 'name' => array('name'), - ), - ); - - $schema['image_effects'] = array( - 'description' => 'Stores configuration options for image effects.', - 'fields' => array( - 'ieid' => array( - 'description' => 'The primary identifier for an image effect.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'isid' => array( - 'description' => 'The {image_styles}.isid for an image style.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'weight' => array( - 'description' => 'The weight of the effect in the style.', - 'type' => 'int', - 'unsigned' => FALSE, - 'not null' => TRUE, - 'default' => 0, - ), - 'name' => array( - 'description' => 'The unique name of the effect to be executed.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - ), - 'data' => array( - 'description' => 'The configuration data for the effect.', - 'type' => 'blob', - 'not null' => TRUE, - 'size' => 'big', - 'serialize' => TRUE, - ), - ), - 'primary key' => array('ieid'), - 'indexes' => array( - 'isid' => array('isid'), - 'weight' => array('weight'), - ), - 'foreign keys' => array( - 'image_style' => array( - 'table' => 'image_styles', - 'columns' => array('isid' => 'isid'), - ), - ), - ); - - return $schema; -} - -/** * Implements hook_field_schema(). */ function image_field_schema($field) { diff --git a/core/modules/image/image.module b/core/modules/image/image.module index 4d7b967..2c0a42a 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -135,15 +135,6 @@ function image_menu() { 'access arguments' => array('administer image styles'), 'file' => 'image.admin.inc', ); - $items['admin/config/media/image-styles/revert/%image_style'] = array( - 'title' => 'Revert style', - 'description' => 'Revert an image style.', - 'load arguments' => array(NULL, (string) IMAGE_STORAGE_OVERRIDE), - 'page callback' => 'drupal_get_form', - 'page arguments' => array('image_style_revert_form', 5), - 'access arguments' => array('administer image styles'), - 'file' => 'image.admin.inc', - ); $items['admin/config/media/image-styles/edit/%image_style/effects/%image_effect'] = array( 'title' => 'Edit image effect', 'description' => 'Edit an existing effect within a style.', @@ -331,45 +322,6 @@ function image_file_predelete($file) { } /** - * Implements hook_image_default_styles(). - */ -function image_image_default_styles() { - $styles = array(); - - $styles['thumbnail'] = array( - 'effects' => array( - array( - 'name' => 'image_scale', - 'data' => array('width' => 100, 'height' => 100, 'upscale' => 1), - 'weight' => 0, - ), - ) - ); - - $styles['medium'] = array( - 'effects' => array( - array( - 'name' => 'image_scale', - 'data' => array('width' => 220, 'height' => 220, 'upscale' => 1), - 'weight' => 0, - ), - ) - ); - - $styles['large'] = array( - 'effects' => array( - array( - 'name' => 'image_scale', - 'data' => array('width' => 480, 'height' => 480, 'upscale' => 0), - 'weight' => 0, - ), - ) - ); - - return $styles; -} - -/** * Implements hook_image_style_save(). */ function image_image_style_save($style) { @@ -483,55 +435,30 @@ function image_path_flush($path) { * @see image_style_load() */ function image_styles() { - $styles = &drupal_static(__FUNCTION__); - - // Grab from cache or build the array. - if (!isset($styles)) { - if ($cache = cache()->get('image_styles')) { - $styles = $cache->data; - } - else { + // @todo Configuration must not be statically cached nor cache-system cached. + // However, there's a drupal_alter() involved here. + +// $styles = &drupal_static(__FUNCTION__); +// +// // Grab from cache or build the array. +// if (!isset($styles)) { +// if ($cache = cache()->get('image_styles')) { +// $styles = $cache->data; +// } +// else { $styles = array(); - // Select the module-defined styles. - foreach (module_implements('image_default_styles') as $module) { - $module_styles = module_invoke($module, 'image_default_styles'); - foreach ($module_styles as $style_name => $style) { - $style['name'] = $style_name; - $style['module'] = $module; - $style['storage'] = IMAGE_STORAGE_DEFAULT; - foreach ($style['effects'] as $key => $effect) { - $definition = image_effect_definition_load($effect['name']); - $effect = array_merge($definition, $effect); - $style['effects'][$key] = $effect; - } - $styles[$style_name] = $style; - } - } - - // Select all the user-defined styles. - $user_styles = db_select('image_styles', NULL, array('fetch' => PDO::FETCH_ASSOC)) - ->fields('image_styles') - ->orderBy('name') - ->execute() - ->fetchAllAssoc('name', PDO::FETCH_ASSOC); - - // Allow the user styles to override the module styles. - foreach ($user_styles as $style_name => $style) { - $style['module'] = NULL; - $style['storage'] = IMAGE_STORAGE_NORMAL; - $style['effects'] = image_style_effects($style); - if (isset($styles[$style_name]['module'])) { - $style['module'] = $styles[$style_name]['module']; - $style['storage'] = IMAGE_STORAGE_OVERRIDE; - } - $styles[$style_name] = $style; + // Select the styles we have configured. + $configured_styles = config_get_verified_storage_names_with_prefix('image.styles'); + foreach ($configured_styles as $config_name) { + // @todo Allow to retrieve the name without prefix only. + $style = image_style_load(str_replace('image.styles.', '', $config_name)); + $styles[$style['name']] = $style; } - drupal_alter('image_styles', $styles); - cache()->set('image_styles', $styles); - } - } +// cache()->set('image_styles', $styles); +// } +// } return $styles; } @@ -541,45 +468,31 @@ function image_styles() { * * @param $name * The name of the style. - * @param $isid - * Optional. The numeric id of a style if the name is not known. - * @param $include - * If set, this loader will restrict to a specific type of image style, may be - * one of the defined Image style storage constants. * @return * An image style array containing the following keys: - * - "isid": The unique image style ID. * - "name": The unique image style name. * - "effects": An array of image effects within this image style. - * If the image style name or ID is not valid, an empty array is returned. + * If the image style name is not valid, an empty array is returned. * @see image_effect_load() */ -function image_style_load($name = NULL, $isid = NULL, $include = NULL) { - $styles = image_styles(); - - // If retrieving by name. - if (isset($name) && isset($styles[$name])) { - $style = $styles[$name]; - } +function image_style_load($name) { + $style = config('image.styles.' . $name)->get(); - // If retrieving by image style id. - if (!isset($name) && isset($isid)) { - foreach ($styles as $name => $database_style) { - if (isset($database_style['isid']) && $database_style['isid'] == $isid) { - $style = $database_style; - break; - } - } + // @todo Requires a more reliable + generic method to check for whether the + // configuration object exists. + if (!isset($style['name'])) { + return FALSE; } - // Restrict to the specific type of flag. This bitwise operation basically - // states "if the storage is X, then allow". - if (isset($style) && (!isset($include) || ($style['storage'] & (int) $include))) { - return $style; + foreach ($style['effects'] as $ieid => $effect) { + $definition = image_effect_definition_load($effect['name']); + $effect = array_merge($definition, $effect); + $style['effects'][$ieid] = $effect; } + // Sort effects by weight. + uasort($style['effects'], 'drupal_sort_weight'); - // Otherwise the style was not found. - return FALSE; + return $style; } /** @@ -588,22 +501,21 @@ function image_style_load($name = NULL, $isid = NULL, $include = NULL) { * @param style * An image style array. * @return - * An image style array. In the case of a new style, 'isid' will be populated. + * An image style array. */ function image_style_save($style) { - if (isset($style['isid']) && is_numeric($style['isid'])) { - // Load the existing style to make sure we account for renamed styles. - $old_style = image_style_load(NULL, $style['isid']); - image_style_flush($old_style); - drupal_write_record('image_styles', $style, 'isid'); - if ($old_style['name'] != $style['name']) { - $style['old_name'] = $old_style['name']; - } + $config = config('image.styles.' . $style['name']); + $config->set('name', $style['name']); + if (isset($style['effects'])) { + $config->set('effects', $style['effects']); } else { - drupal_write_record('image_styles', $style); - $style['is_new'] = TRUE; + $config->set('effects', array()); } + $config->save(); + // @todo is_new must only be set when the configuration object did not exist + // yet. + $style['is_new'] = TRUE; // Let other modules update as necessary on save. module_invoke_all('image_style_save', $style); @@ -628,8 +540,8 @@ function image_style_save($style) { function image_style_delete($style, $replacement_style_name = '') { image_style_flush($style); - db_delete('image_effects')->condition('isid', $style['isid'])->execute(); - db_delete('image_styles')->condition('isid', $style['isid'])->execute(); + $config = config('image.styles.' . $style['name']); + $config->delete(); // Let other modules update as necessary on save. $style['old_name'] = $style['name']; @@ -648,17 +560,11 @@ function image_style_delete($style, $replacement_style_name = '') { * An array of image effects associated with specified image style in the * format array('isid' => array()), or an empty array if the specified style * has no effects. + * + * @todo Remove this function; it's entirely obsolete. */ function image_style_effects($style) { - $effects = image_effects(); - $style_effects = array(); - foreach ($effects as $effect) { - if ($style['isid'] == $effect['isid']) { - $style_effects[$effect['ieid']] = $effect; - } - } - - return $style_effects; + return $style['effects']; } /** @@ -854,11 +760,11 @@ function image_style_flush($style) { // Let other modules update as necessary on flush. module_invoke_all('image_style_flush', $style); - // Clear image style and effect caches. - cache()->delete('image_styles'); - cache()->deletePrefix('image_effects:'); - drupal_static_reset('image_styles'); - drupal_static_reset('image_effects'); +// // Clear image style and effect caches. +// cache()->delete('image_styles'); +// cache()->deletePrefix('image_effects:'); +// drupal_static_reset('image_styles'); +// drupal_static_reset('image_effects'); // Clear field caches so that formatters may be added for this style. field_info_cache_clear(); @@ -876,15 +782,15 @@ function image_style_flush($style) { * * @param $style_name * The name of the style to be used with this image. - * @param $uri + * @param $path * The path to the image. * @return * The absolute URL where a style image can be downloaded, suitable for use * in an tag. Requesting the URL will cause the image to be created. * @see image_style_deliver() */ -function image_style_url($style_name, $uri) { - $uri = image_style_path($style_name, $uri); +function image_style_url($style_name, $path) { + $uri = image_style_path($style_name, $path); // If not using clean URLs, the image derivative callback is only available // with the query string. If the file does not exist, use url() to ensure @@ -925,44 +831,6 @@ function image_style_path($style_name, $uri) { } /** - * Save a default image style to the database. - * - * @param style - * An image style array provided by a module. - * @return - * An image style array. The returned style array will include the new 'isid' - * assigned to the style. - */ -function image_default_style_save($style) { - $style = image_style_save($style); - $effects = array(); - foreach ($style['effects'] as $effect) { - $effect['isid'] = $style['isid']; - $effect = image_effect_save($effect); - $effects[$effect['ieid']] = $effect; - } - $style['effects'] = $effects; - return $style; -} - -/** - * Revert the changes made by users to a default image style. - * - * @param style - * An image style array. - * @return - * Boolean TRUE if the operation succeeded. - */ -function image_default_style_revert($style) { - image_style_flush($style); - - db_delete('image_effects')->condition('isid', $style['isid'])->execute(); - db_delete('image_styles')->condition('isid', $style['isid'])->execute(); - - return TRUE; -} - -/** * Pull in image effects exposed by modules implementing hook_image_effect_info(). * * @return @@ -1015,8 +883,6 @@ function image_effect_definitions() { * * @param $effect * The name of the effect definition to load. - * @param $style - * An image style array to which this effect will be added. * @return * An array containing the image effect definition with the following keys: * - "effect": The unique name for the effect being performed. Usually prefixed @@ -1028,18 +894,8 @@ function image_effect_definitions() { * - "summary": (optional) The name of a theme function that will display a * one-line summary of the effect. Does not include the "theme_" prefix. */ -function image_effect_definition_load($effect, $style_name = NULL) { +function image_effect_definition_load($effect) { $definitions = image_effect_definitions(); - - // If a style is specified, do not allow loading of default style - // effects. - if (isset($style_name)) { - $style = image_style_load($style_name, NULL); - if ($style['storage'] == IMAGE_STORAGE_DEFAULT) { - return FALSE; - } - } - return isset($definitions[$effect]) ? $definitions[$effect] : FALSE; } @@ -1049,6 +905,8 @@ function image_effect_definition_load($effect, $style_name = NULL) { * @return * An array of all image effects. * @see image_effect_load() + * + * @todo Remove after moving/resolving the todo. */ function image_effects() { $effects = &drupal_static(__FUNCTION__); @@ -1057,6 +915,9 @@ function image_effects() { $effects = array(); // Add database image effects. + // @todo Strictly speaking, this is obsolete. However, it demonstrates a + // use-case for retrieving/listing configuration objects using a wildcard + // within the name (instead of only the suffix). $result = db_select('image_effects', NULL, array('fetch' => PDO::FETCH_ASSOC)) ->fields('image_effects') ->orderBy('image_effects.weight', 'ASC') @@ -1082,13 +943,10 @@ function image_effects() { * The image effect ID. * @param $style_name * The image style name. - * @param $include - * If set, this loader will restrict to a specific type of image style, may be - * one of the defined Image style storage constants. + * * @return * An image effect array, consisting of the following keys: * - "ieid": The unique image effect ID. - * - "isid": The unique image style ID that contains this image effect. * - "weight": The weight of this image effect within the image style. * - "name": The name of the effect definition that powers this image effect. * - "data": An array of configuration options for this image effect. @@ -1098,9 +956,20 @@ function image_effects() { * @see image_style_load() * @see image_effect_definition_load() */ -function image_effect_load($ieid, $style_name, $include = NULL) { - if (($style = image_style_load($style_name, NULL, $include)) && isset($style['effects'][$ieid])) { - return $style['effects'][$ieid]; +function image_effect_load($ieid, $style_name) { + if (($style = image_style_load($style_name)) && isset($style['effects'][$ieid])) { + $effect = $style['effects'][$ieid]; + $definition = image_effect_definition_load($effect['name']); + $effect = array_merge($definition, $effect); + // @todo The effect's key name within the style is unknown. It *should* be + // identical to the ieid, but that is in no way guaranteed. And of course, + // the ieid key *within* the effect is senseless duplication in the first + // place. This problem can be eliminated in many places, but especially + // for loaded menu arguments like %image_effect, the actual router + // callbacks don't have access to 'ieid' anymore (unless resorting to + // dirty %index and %map tricks). + $effect['ieid'] = $ieid; + return $effect; } return FALSE; } @@ -1108,19 +977,32 @@ function image_effect_load($ieid, $style_name, $include = NULL) { /** * Save an image effect. * + * @param $style_name + * The image style this effect belongs to. * @param $effect * An image effect array. * @return * An image effect array. In the case of a new effect, 'ieid' will be set. */ -function image_effect_save($effect) { - if (!empty($effect['ieid'])) { - drupal_write_record('image_effects', $effect, 'ieid'); - } - else { - drupal_write_record('image_effects', $effect); +function image_effect_save($style_name, $effect) { + $config = config('image.styles.' . $style_name); + + if (!isset($effect['ieid']) || empty($effect['ieid'])) { + // We need to generate the ieid and save the new effect. + // The machine name is all the elements of the data array concatenated + // together, delimited by underscores. + $effect['ieid'] = $effect['name']; + foreach ($effect['data'] as $key => $value) { + $effect['ieid'] .= '_' . $value; + } + // @todo The machine name must not use any special non-alphanumeric + // characters, and may also not contain dots/periods, as that is the + // config system's nested key syntax. + $effect['ieid'] = preg_replace('@[^a-zA-Z0-9_-]@', '', $effect['ieid']); } - $style = image_style_load(NULL, $effect['isid']); + $config->set('effects.' . $effect['ieid'], $effect); + $config->save(); + $style = image_style_load($style_name); image_style_flush($style); return $effect; } @@ -1128,12 +1010,16 @@ function image_effect_save($effect) { /** * Delete an image effect. * + * @param $style_name + * The image style this effect belongs to. * @param $effect * An image effect array. */ -function image_effect_delete($effect) { - db_delete('image_effects')->condition('ieid', $effect['ieid'])->execute(); - $style = image_style_load(NULL, $effect['isid']); +function image_effect_delete($style_name, $effect) { + $config = config('image.styles.' . $style_name); + $config->clear('effects.' . $effect['ieid']); + $config->save(); + $style = image_style_load($style_name); image_style_flush($style); } diff --git a/core/modules/image/image.test b/core/modules/image/image.test index 4d4532c..7a4683b 100644 --- a/core/modules/image/image.test +++ b/core/modules/image/image.test @@ -468,7 +468,7 @@ class ImageAdminStylesUnitTest extends ImageFieldTestCase { // Load the style by the new name with the new weights. drupal_static_reset('image_styles'); - $style = image_style_load($style_name, NULL); + $style = image_style_load($style_name); // Confirm the new style order was saved. $effect_edits_order = array_reverse($effect_edits_order); @@ -507,76 +507,6 @@ class ImageAdminStylesUnitTest extends ImageFieldTestCase { } /** - * Test to override, edit, then revert a style. - */ - function testDefaultStyle() { - // Setup a style to be created and effects to add to it. - $style_name = 'thumbnail'; - $edit_path = 'admin/config/media/image-styles/edit/' . $style_name; - $delete_path = 'admin/config/media/image-styles/delete/' . $style_name; - $revert_path = 'admin/config/media/image-styles/revert/' . $style_name; - - // Ensure deleting a default is not possible. - $this->drupalGet($delete_path); - $this->assertText(t('Page not found'), t('Default styles may not be deleted.')); - - // Ensure that editing a default is not possible (without overriding). - $this->drupalGet($edit_path); - $this->assertNoField('edit-name', t('Default styles may not be renamed.')); - $this->assertNoField('edit-submit', t('Default styles may not be edited.')); - $this->assertNoField('edit-add', t('Default styles may not have new effects added.')); - - // Create an image to make sure the default works before overriding. - drupal_static_reset('image_styles'); - $style = image_style_load($style_name); - $image_path = $this->createSampleImage($style); - $this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); - - // Verify that effects attached to a default style do not have an ieid key. - foreach ($style['effects'] as $effect) { - $this->assertFalse(isset($effect['ieid']), t('The %effect effect does not have an ieid.', array('%effect' => $effect['name']))); - } - - // Override the default. - $this->drupalPost($edit_path, array(), t('Override defaults')); - $this->assertRaw(t('The %style style has been overridden, allowing you to change its settings.', array('%style' => $style_name)), t('Default image style may be overridden.')); - - // Add sample effect to the overridden style. - $this->drupalPost($edit_path, array('new' => 'image_desaturate'), t('Add')); - drupal_static_reset('image_styles'); - $style = image_style_load($style_name); - - // Verify that effects attached to the style have an ieid now. - foreach ($style['effects'] as $effect) { - $this->assertTrue(isset($effect['ieid']), t('The %effect effect has an ieid.', array('%effect' => $effect['name']))); - } - - // The style should now have 2 effect, the original scale provided by core - // and the desaturate effect we added in the override. - $effects = array_values($style['effects']); - $this->assertEqual($effects[0]['name'], 'image_scale', t('The default effect still exists in the overridden style.')); - $this->assertEqual($effects[1]['name'], 'image_desaturate', t('The added effect exists in the overridden style.')); - - // Check that we are unable to rename an overridden style. - $this->drupalGet($edit_path); - $this->assertNoField('edit-name', t('Overridden styles may not be renamed.')); - - // Create an image to ensure the override works properly. - $image_path = $this->createSampleImage($style); - $this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); - - // Revert the image style. - $this->drupalPost($revert_path, array(), t('Revert')); - drupal_static_reset('image_styles'); - $style = image_style_load($style_name); - - // The style should now have the single effect for scale. - $effects = array_values($style['effects']); - $this->assertEqual($effects[0]['name'], 'image_scale', t('The default effect still exists in the reverted style.')); - $this->assertFalse(array_key_exists(1, $effects), t('The added effect has been removed in the reverted style.')); - } - - /** * Test deleting a style and choosing a replacement style. */ function testStyleReplacement() { @@ -935,6 +865,7 @@ class ImageFieldValidateTestCase extends ImageFieldTestCase { * Tests that images have correct dimensions when styled. */ class ImageDimensionsUnitTest extends DrupalWebTestCase { + protected $profile = 'testing'; public static function getInfo() { return array( @@ -945,7 +876,7 @@ class ImageDimensionsUnitTest extends DrupalWebTestCase { } function setUp() { - parent::setUp('image_module_test'); + parent::setUp('rdf', 'image', 'image_module_test'); } /** @@ -968,6 +899,10 @@ class ImageDimensionsUnitTest extends DrupalWebTestCase { 'width' => 40, 'height' => 20, ); + // Verify that the original image matches the hard-coded values. + $image_info = image_get_info($original_uri); + $this->assertEqual($image_info['width'], $variables['width']); + $this->assertEqual($image_info['height'], $variables['height']); // Scale an image that is wider than it is high. $effect = array( @@ -977,19 +912,19 @@ class ImageDimensionsUnitTest extends DrupalWebTestCase { 'height' => 90, 'upscale' => TRUE, ), - 'isid' => $style['isid'], + 'weight' => 0, ); - image_effect_save($effect); + image_effect_save('test', $effect); $img_tag = theme_image_style($variables); - $this->assertEqual($img_tag, '', t('Expected img tag was found.')); + $this->assertEqual($img_tag, ''); $this->assertFalse(file_exists($generated_uri), t('Generated file does not exist.')); $this->drupalGet($url); $this->assertResponse(200, t('Image was generated at the URL.')); $this->assertTrue(file_exists($generated_uri), t('Generated file does exist after we accessed it.')); $image_info = image_get_info($generated_uri); - $this->assertEqual($image_info['width'], 120, t('Expected width was found.')); - $this->assertEqual($image_info['height'], 60, t('Expected height was found.')); + $this->assertEqual($image_info['width'], 120); + $this->assertEqual($image_info['height'], 60); // Rotate 90 degrees anticlockwise. $effect = array( @@ -998,19 +933,19 @@ class ImageDimensionsUnitTest extends DrupalWebTestCase { 'degrees' => -90, 'random' => FALSE, ), - 'isid' => $style['isid'], + 'weight' => 1, ); - image_effect_save($effect); + image_effect_save('test', $effect); $img_tag = theme_image_style($variables); - $this->assertEqual($img_tag, '', t('Expected img tag was found.')); + $this->assertEqual($img_tag, ''); $this->assertFalse(file_exists($generated_uri), t('Generated file does not exist.')); $this->drupalGet($url); $this->assertResponse(200, t('Image was generated at the URL.')); $this->assertTrue(file_exists($generated_uri), t('Generated file does exist after we accessed it.')); $image_info = image_get_info($generated_uri); - $this->assertEqual($image_info['width'], 60, t('Expected width was found.')); - $this->assertEqual($image_info['height'], 120, t('Expected height was found.')); + $this->assertEqual($image_info['width'], 60); + $this->assertEqual($image_info['height'], 120); // Scale an image that is higher than it is wide (rotated by previous effect). $effect = array( @@ -1020,19 +955,19 @@ class ImageDimensionsUnitTest extends DrupalWebTestCase { 'height' => 90, 'upscale' => TRUE, ), - 'isid' => $style['isid'], + 'weight' => 2, ); - image_effect_save($effect); + image_effect_save('test', $effect); $img_tag = theme_image_style($variables); - $this->assertEqual($img_tag, '', t('Expected img tag was found.')); + $this->assertEqual($img_tag, ''); $this->assertFalse(file_exists($generated_uri), t('Generated file does not exist.')); $this->drupalGet($url); $this->assertResponse(200, t('Image was generated at the URL.')); $this->assertTrue(file_exists($generated_uri), t('Generated file does exist after we accessed it.')); $image_info = image_get_info($generated_uri); - $this->assertEqual($image_info['width'], 45, t('Expected width was found.')); - $this->assertEqual($image_info['height'], 90, t('Expected height was found.')); + $this->assertEqual($image_info['width'], 45); + $this->assertEqual($image_info['height'], 90); // Test upscale disabled. $effect = array( @@ -1042,37 +977,37 @@ class ImageDimensionsUnitTest extends DrupalWebTestCase { 'height' => 200, 'upscale' => FALSE, ), - 'isid' => $style['isid'], + 'weight' => 3, ); - image_effect_save($effect); + image_effect_save('test', $effect); $img_tag = theme_image_style($variables); - $this->assertEqual($img_tag, '', t('Expected img tag was found.')); + $this->assertEqual($img_tag, ''); $this->assertFalse(file_exists($generated_uri), t('Generated file does not exist.')); $this->drupalGet($url); $this->assertResponse(200, t('Image was generated at the URL.')); $this->assertTrue(file_exists($generated_uri), t('Generated file does exist after we accessed it.')); $image_info = image_get_info($generated_uri); - $this->assertEqual($image_info['width'], 45, t('Expected width was found.')); - $this->assertEqual($image_info['height'], 90, t('Expected height was found.')); + $this->assertEqual($image_info['width'], 45); + $this->assertEqual($image_info['height'], 90); // Add a desaturate effect. $effect = array( 'name' => 'image_desaturate', 'data' => array(), - 'isid' => $style['isid'], + 'weight' => 4, ); - image_effect_save($effect); + image_effect_save('test', $effect); $img_tag = theme_image_style($variables); - $this->assertEqual($img_tag, '', t('Expected img tag was found.')); + $this->assertEqual($img_tag, ''); $this->assertFalse(file_exists($generated_uri), t('Generated file does not exist.')); $this->drupalGet($url); $this->assertResponse(200, t('Image was generated at the URL.')); $this->assertTrue(file_exists($generated_uri), t('Generated file does exist after we accessed it.')); $image_info = image_get_info($generated_uri); - $this->assertEqual($image_info['width'], 45, t('Expected width was found.')); - $this->assertEqual($image_info['height'], 90, t('Expected height was found.')); + $this->assertEqual($image_info['width'], 45); + $this->assertEqual($image_info['height'], 90); // Add a random rotate effect. $effect = array( @@ -1081,12 +1016,12 @@ class ImageDimensionsUnitTest extends DrupalWebTestCase { 'degrees' => 180, 'random' => TRUE, ), - 'isid' => $style['isid'], + 'weight' => 5, ); - image_effect_save($effect); + image_effect_save('test', $effect); $img_tag = theme_image_style($variables); - $this->assertEqual($img_tag, '', t('Expected img tag was found.')); + $this->assertEqual($img_tag, ''); $this->assertFalse(file_exists($generated_uri), t('Generated file does not exist.')); $this->drupalGet($url); $this->assertResponse(200, t('Image was generated at the URL.')); @@ -1101,19 +1036,19 @@ class ImageDimensionsUnitTest extends DrupalWebTestCase { 'height' => 30, 'anchor' => 'center-center', ), - 'isid' => $style['isid'], + 'weight' => 6, ); - image_effect_save($effect); + image_effect_save('test', $effect); $img_tag = theme_image_style($variables); - $this->assertEqual($img_tag, '', t('Expected img tag was found.')); + $this->assertEqual($img_tag, ''); $this->assertFalse(file_exists($generated_uri), t('Generated file does not exist.')); $this->drupalGet($url); $this->assertResponse(200, t('Image was generated at the URL.')); $this->assertTrue(file_exists($generated_uri), t('Generated file does exist after we accessed it.')); $image_info = image_get_info($generated_uri); - $this->assertEqual($image_info['width'], 30, t('Expected width was found.')); - $this->assertEqual($image_info['height'], 30, t('Expected height was found.')); + $this->assertEqual($image_info['width'], 30); + $this->assertEqual($image_info['height'], 30); // Rotate to a non-multiple of 90 degrees. $effect = array( @@ -1122,30 +1057,30 @@ class ImageDimensionsUnitTest extends DrupalWebTestCase { 'degrees' => 57, 'random' => FALSE, ), - 'isid' => $style['isid'], + 'weight' => 7, ); - $effect = image_effect_save($effect); + $effect = image_effect_save('test', $effect); $img_tag = theme_image_style($variables); - $this->assertEqual($img_tag, '', t('Expected img tag was found.')); + $this->assertEqual($img_tag, ''); $this->assertFalse(file_exists($generated_uri), t('Generated file does not exist.')); $this->drupalGet($url); $this->assertResponse(200, t('Image was generated at the URL.')); $this->assertTrue(file_exists($generated_uri), t('Generated file does exist after we accessed it.')); - image_effect_delete($effect); + image_effect_delete('test', $effect); // Ensure that an effect with no dimensions callback unsets the dimensions. // This ensures compatibility with 7.0 contrib modules. $effect = array( 'name' => 'image_module_test_null', 'data' => array(), - 'isid' => $style['isid'], + 'weight' => 8, ); - image_effect_save($effect); + image_effect_save('test', $effect); $img_tag = theme_image_style($variables); - $this->assertEqual($img_tag, '', t('Expected img tag was found.')); + $this->assertEqual($img_tag, ''); } } diff --git a/core/modules/locale/locale.test b/core/modules/locale/locale.test index 42a0fbd..d66a546 100644 --- a/core/modules/locale/locale.test +++ b/core/modules/locale/locale.test @@ -2333,7 +2333,7 @@ class LocaleUILanguageNegotiationTest extends DrupalWebTestCase { } /** - * Tests url() when separate domains are used for multiple languages. + * Test if the url function returns the right url when using different domains for different languages. */ function testLanguageDomain() { // Add the Italian language. diff --git a/core/modules/poll/poll.test b/core/modules/poll/poll.test index 0c406f0..adec664 100644 --- a/core/modules/poll/poll.test +++ b/core/modules/poll/poll.test @@ -496,7 +496,9 @@ class PollVoteCheckHostname extends PollTestCase { // Enable page cache to verify that the result page is not saved in the // cache when anonymous voting is allowed. - variable_set('cache', 1); + $config = config('system.performance'); + $config->set('cache', 1); + $config->save(); // Create poll. $title = $this->randomName(); diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php index c9fc562..8f2e0b3 100644 --- a/core/modules/simpletest/drupal_web_test_case.php +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -1310,6 +1310,8 @@ class DrupalWebTestCase extends DrupalTestCase { // Store necessary current values before switching to prefixed database. $this->originalLanguage = $language; $this->originalLanguageDefault = variable_get('language_default'); + $this->originalConfigDirectory = $GLOBALS['config_directory_name']; + $this->originalConfigSignatureKey = $GLOBALS['config_signature_key']; $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); $this->originalProfile = drupal_get_profile(); $clean_url_original = variable_get('clean_url', 0); @@ -1346,6 +1348,14 @@ class DrupalWebTestCase extends DrupalTestCase { file_prepare_directory($temp_files_directory, FILE_CREATE_DIRECTORY); $this->generatedTestFiles = FALSE; + // Create and set a new configuration directory and signature key. + // The child site automatically adjusts the global $config_directory_name to + // a test-prefix-specific directory within the public files directory. + $GLOBALS['config_directory_name'] = 'simpletest/config_' . $this->databasePrefix; + $this->configFileDirectory = $this->originalFileDirectory . '/' . $GLOBALS['config_directory_name']; + file_prepare_directory($this->configFileDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + $GLOBALS['config_signature_key'] = drupal_hash_base64(drupal_random_bytes(55)); + // Log fatal errors. ini_set('log_errors', 1); ini_set('error_log', $public_files_directory . '/error.log'); @@ -1568,6 +1578,10 @@ class DrupalWebTestCase extends DrupalTestCase { // Rebuild caches. $this->refreshVariables(); + // Reset configuration globals. + $GLOBALS['config_directory_name'] = $this->originalConfigDirectory; + $GLOBALS['config_signature_key'] = $this->originalConfigSignatureKey; + // Reset language. $language = $this->originalLanguage; if ($this->originalLanguageDefault) { diff --git a/core/modules/simpletest/tests/bootstrap.test b/core/modules/simpletest/tests/bootstrap.test index 13fdf07..fee8fcc 100644 --- a/core/modules/simpletest/tests/bootstrap.test +++ b/core/modules/simpletest/tests/bootstrap.test @@ -116,7 +116,9 @@ class BootstrapPageCacheTestCase extends DrupalWebTestCase { * Test support for requests containing If-Modified-Since and If-None-Match headers. */ function testConditionalRequests() { - variable_set('cache', 1); + $config = config('system.performance'); + $config->set('cache', 1); + $config->save(); // Fill the cache. $this->drupalGet(''); @@ -154,7 +156,9 @@ class BootstrapPageCacheTestCase extends DrupalWebTestCase { * Test cache headers. */ function testPageCache() { - variable_set('cache', 1); + $config = config('system.performance'); + $config->set('cache', 1); + $config->save(); // Fill the cache. $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); @@ -198,7 +202,9 @@ class BootstrapPageCacheTestCase extends DrupalWebTestCase { * mod_deflate Apache module. */ function testPageCompression() { - variable_set('cache', 1); + $config = config('system.performance'); + $config->set('cache', 1); + $config->save(); // Fill the cache and verify that output is compressed. $this->drupalGet('', array(), array('Accept-Encoding: gzip,deflate')); @@ -291,14 +297,18 @@ class HookBootExitTestCase extends DrupalWebTestCase { */ function testHookBootExit() { // Test with cache disabled. Boot and exit should always fire. - variable_set('cache', 0); + $config = config('system.performance'); + $config->set('cache', 0); + $config->save(); + $this->drupalGet(''); $calls = 1; $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, t('hook_boot called with disabled cache.')); $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, t('hook_exit called with disabled cache.')); // Test with normal cache. Boot and exit should be called. - variable_set('cache', 1); + $config->set('cache', 1); + $config->save(); $this->drupalGet(''); $calls++; $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, t('hook_boot called with normal cache.')); diff --git a/core/modules/simpletest/tests/cache.test b/core/modules/simpletest/tests/cache.test index bca4e25..81f2377 100644 --- a/core/modules/simpletest/tests/cache.test +++ b/core/modules/simpletest/tests/cache.test @@ -95,7 +95,9 @@ class CacheTestCase extends DrupalWebTestCase { * The time in seconds the cache should minimal live. */ protected function setupLifetime($time) { - variable_set('cache_lifetime', $time); + $config = config('system.performance'); + $config->set('cache_lifetime', $time); + $config->save(); variable_set('cache_flush', 0); } } diff --git a/core/modules/simpletest/tests/common.test b/core/modules/simpletest/tests/common.test index c4a7cbe..a681cf1 100644 --- a/core/modules/simpletest/tests/common.test +++ b/core/modules/simpletest/tests/common.test @@ -1171,8 +1171,10 @@ class CommonJavaScriptTestCase extends DrupalWebTestCase { parent::setUp('locale', 'simpletest', 'common_test'); // Disable preprocessing - $this->preprocess_js = variable_get('preprocess_js', 0); - variable_set('preprocess_js', 0); + $config = config('system.performance'); + $this->preprocess_js = $config->get('preprocess_js'); + $config->set('preprocess_js', 0); + $config->save(); // Reset drupal_add_js() and drupal_add_library() statics before each test. drupal_static_reset('drupal_add_js'); @@ -1181,7 +1183,9 @@ class CommonJavaScriptTestCase extends DrupalWebTestCase { function tearDown() { // Restore configured value for JavaScript preprocessing. - variable_set('preprocess_js', $this->preprocess_js); + $config = config('system.performance'); + $config->set('preprocess_js', $this->preprocess_js); + $config->save(); parent::tearDown(); } @@ -1379,7 +1383,9 @@ class CommonJavaScriptTestCase extends DrupalWebTestCase { // Now ensure that with aggregation on, one file is made for the // 'every_page' files, and one file is made for the others. drupal_static_reset('drupal_add_js'); - variable_set('preprocess_js', 1); + $config = config('system.performance'); + $config->set('preprocess_js', 1); + $config->save(); drupal_add_js('core/misc/ajax.js'); drupal_add_js('core/misc/authorize.js', array('every_page' => TRUE)); drupal_add_js('core/misc/autocomplete.js'); diff --git a/core/modules/simpletest/tests/session.test b/core/modules/simpletest/tests/session.test index 746f632..6303ca5 100644 --- a/core/modules/simpletest/tests/session.test +++ b/core/modules/simpletest/tests/session.test @@ -139,7 +139,9 @@ class SessionTestCase extends DrupalWebTestCase { $this->assertSessionEmpty(TRUE); // The same behavior is expected when caching is enabled. - variable_set('cache', 1); + $config = config('system.performance'); + $config->set('cache', 1); + $config->save(); $this->drupalGet(''); $this->assertSessionCookie(FALSE); $this->assertSessionEmpty(TRUE); diff --git a/core/modules/simpletest/tests/theme.test b/core/modules/simpletest/tests/theme.test index cf75885..0ed109d 100644 --- a/core/modules/simpletest/tests/theme.test +++ b/core/modules/simpletest/tests/theme.test @@ -92,7 +92,9 @@ class ThemeUnitTest extends DrupalWebTestCase { // what is output to the HTML HEAD based on what is in a theme's .info file, // so it doesn't matter what page we get, as long as it is themed with the // test theme. First we test with CSS aggregation disabled. - variable_set('preprocess_css', 0); + $config = config('system.performance'); + $config->set('preprocess_css', 0); + $config->save(); $this->drupalGet('theme-test/suggestion'); $this->assertNoText('system.base.css', t('The theme\'s .info file is able to override a module CSS file from being added to the page.')); @@ -100,9 +102,11 @@ class ThemeUnitTest extends DrupalWebTestCase { // triggered during drupal_build_css_cache() when a source file doesn't // exist. Then allow remaining tests to continue with aggregation disabled // by default. - variable_set('preprocess_css', 1); + $config->set('preprocess_css', 1); + $config->save(); $this->drupalGet('theme-test/suggestion'); - variable_set('preprocess_css', 0); + $config->set('preprocess_css', 0); + $config->save(); } /** diff --git a/core/modules/statistics/statistics.test b/core/modules/statistics/statistics.test index 592f165..7fac22c 100644 --- a/core/modules/statistics/statistics.test +++ b/core/modules/statistics/statistics.test @@ -69,7 +69,9 @@ class StatisticsLoggingTestCase extends DrupalWebTestCase { $this->node = $this->drupalCreateNode(array('title' => $this->randomName(255), 'uid' => $this->auth_user->uid)); // Enable page caching. - variable_set('cache', TRUE); + $config = config('system.performance'); + $config->set('cache', 1); + $config->save(); // Enable access logging. variable_set('statistics_enable_access_log', 1); diff --git a/core/modules/system/config/system.performance.xml b/core/modules/system/config/system.performance.xml new file mode 100644 index 0000000..8edf1eb --- /dev/null +++ b/core/modules/system/config/system.performance.xml @@ -0,0 +1,9 @@ + + + 0 + 0 + 0 + 0 + 0 + 0 + diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index d28ecfb..abe1577 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -428,13 +428,15 @@ function system_theme_settings($form, &$form_state, $key = '') { $form['logo'] = array( '#type' => 'fieldset', '#title' => t('Logo image settings'), + '#description' => t('If toggled on, the following logo will be displayed.'), '#attributes' => array('class' => array('theme-settings-bottom')), ); $form['logo']['default_logo'] = array( '#type' => 'checkbox', - '#title' => t('Use the default logo supplied by the theme'), + '#title' => t('Use the default logo'), '#default_value' => theme_get_setting('default_logo', $key), '#tree' => FALSE, + '#description' => t('Check here if you want the theme to use the logo supplied with it.') ); $form['logo']['settings'] = array( '#type' => 'container', @@ -445,10 +447,17 @@ function system_theme_settings($form, &$form_state, $key = '') { ), ), ); + $logo_path = theme_get_setting('logo_path', $key); + // If $logo_path is a public:// URI, display the path relative to the files + // directory; stream wrappers are not end-user friendly. + if (file_uri_scheme($logo_path) == 'public') { + $logo_path = file_uri_target($logo_path); + } $form['logo']['settings']['logo_path'] = array( '#type' => 'textfield', '#title' => t('Path to custom logo'), - '#default_value' => theme_get_setting('logo_path', $key), + '#default_value' => $logo_path, + '#description' => t('The path to the file you would like to use as your logo file instead of the default logo.'), ); $form['logo']['settings']['logo_upload'] = array( '#type' => 'file', @@ -466,8 +475,9 @@ function system_theme_settings($form, &$form_state, $key = '') { ); $form['favicon']['default_favicon'] = array( '#type' => 'checkbox', - '#title' => t('Use the default shortcut icon supplied by the theme'), + '#title' => t('Use the default shortcut icon.'), '#default_value' => theme_get_setting('default_favicon', $key), + '#description' => t('Check here if you want the theme to use the default shortcut icon.') ); $form['favicon']['settings'] = array( '#type' => 'container', @@ -478,10 +488,17 @@ function system_theme_settings($form, &$form_state, $key = '') { ), ), ); + $favicon_path = theme_get_setting('favicon_path', $key); + // If $favicon_path is a public:// URI, display the path relative to the + // files directory; stream wrappers are not end-user friendly. + if (file_uri_scheme($favicon_path) == 'public') { + $favicon_path = file_uri_target($favicon_path); + } $form['favicon']['settings']['favicon_path'] = array( '#type' => 'textfield', '#title' => t('Path to custom icon'), - '#default_value' => theme_get_setting('favicon_path', $key), + '#default_value' => $favicon_path, + '#description' => t('The path to the image file you would like to use as your custom shortcut icon.') ); $form['favicon']['settings']['favicon_upload'] = array( '#type' => 'file', @@ -490,40 +507,6 @@ function system_theme_settings($form, &$form_state, $key = '') { ); } - // Inject human-friendly values and form element descriptions for logo and - // favicon. - foreach (array('logo' => 'logo.png', 'favicon' => 'favicon.ico') as $type => $default) { - if (isset($form[$type]['settings'][$type . '_path'])) { - $element = &$form[$type]['settings'][$type . '_path']; - - // If path is a public:// URI, display the path relative to the files - // directory; stream wrappers are not end-user friendly. - $original_path = $element['#default_value']; - $friendly_path = NULL; - if (file_uri_scheme($original_path) == 'public') { - $friendly_path = file_uri_target($original_path); - $element['#default_value'] = $friendly_path; - } - - // Prepare local file path for description. - if ($original_path && isset($friendly_path)) { - $local_file = strtr($original_path, array('public:/' => variable_get('file_public_path', conf_path() . '/files'))); - } - elseif ($key) { - $local_file = drupal_get_path('theme', $key) . '/' . $default; - } - else { - $local_file = path_to_theme() . '/' . $default; - } - - $element['#description'] = t('Examples: @implicit-public-file (for a file in the public filesystem), @explicit-file, or @local-file.', array( - '@implicit-public-file' => isset($friendly_path) ? $friendly_path : $default, - '@explicit-file' => file_uri_scheme($original_path) !== FALSE ? $original_path : 'public://' . $default, - '@local-file' => $local_file, - )); - } - } - if ($key) { // Call engine-specific settings. $function = $themes[$key]->prefix . '_engine_settings'; @@ -651,20 +634,13 @@ function system_theme_settings_validate($form, &$form_state) { * the path could not be validated. */ function _system_theme_settings_validate_path($path) { - // Absolute local file paths are invalid. - if (drupal_realpath($path) == $path) { - return FALSE; - } - // A path relative to the Drupal root or a fully qualified URI is valid. - if (is_file($path)) { + if (drupal_realpath($path)) { + // The path is relative to the Drupal root, or is a valid URI. return $path; } - // Prepend 'public://' for relative file paths within public filesystem. - if (file_uri_scheme($path) === FALSE) { - $path = 'public://' . $path; - } - if (is_file($path)) { - return $path; + $uri = 'public://' . $path; + if (file_exists($uri)) { + return $uri; } return FALSE; } @@ -1641,10 +1617,11 @@ function system_logging_settings() { * Form builder; Configure site performance settings. * * @ingroup forms - * @see system_settings_form() + * @see system_performance_settings_submit(). */ -function system_performance_settings() { +function system_performance_settings($form, &$form_state) { drupal_add_js(drupal_get_path('module', 'system') . '/system.js'); + $config = config('system.performance'); $form['clear_cache'] = array( '#type' => 'fieldset', @@ -1662,11 +1639,10 @@ function system_performance_settings() { '#title' => t('Caching'), ); - $cache = variable_get('cache', 0); $form['caching']['cache'] = array( '#type' => 'checkbox', '#title' => t('Cache pages for anonymous users'), - '#default_value' => $cache, + '#default_value' => $config->get('cache'), '#weight' => -2, ); $period = drupal_map_assoc(array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400), 'format_interval'); @@ -1674,16 +1650,16 @@ function system_performance_settings() { $form['caching']['cache_lifetime'] = array( '#type' => 'select', '#title' => t('Minimum cache lifetime'), - '#default_value' => variable_get('cache_lifetime', 0), + '#default_value' => $config->get('cache_lifetime'), '#options' => $period, - '#description' => t('Cached pages will not be re-created until at least this much time has elapsed.') + '#description' => t('Cached pages will not be re-created until at least this much time has elapsed.'), ); $form['caching']['page_cache_maximum_age'] = array( '#type' => 'select', '#title' => t('Expiration of cached pages'), - '#default_value' => variable_get('page_cache_maximum_age', 0), + '#default_value' => $config->get('page_cache_maximum_age'), '#options' => $period, - '#description' => t('The maximum time an external cache can use an old version of a page.') + '#description' => t('The maximum time an external cache can use an old version of a page.'), ); $directory = 'public://'; @@ -1700,34 +1676,57 @@ function system_performance_settings() { '#description' => t('External resources can be optimized automatically, which can reduce both the size and number of requests made to your website.') . $disabled_message, ); - $js_hide = $cache ? '' : ' class="js-hide"'; + $js_hide = $config->get('cache') ? '' : ' class="js-hide"'; $form['bandwidth_optimization']['page_compression'] = array( '#type' => 'checkbox', '#title' => t('Compress cached pages.'), - '#default_value' => variable_get('page_compression', TRUE), + '#default_value' => $config->get('page_compression'), '#prefix' => '
', '#suffix' => '
', ); $form['bandwidth_optimization']['preprocess_css'] = array( '#type' => 'checkbox', '#title' => t('Aggregate and compress CSS files.'), - '#default_value' => intval(variable_get('preprocess_css', 0) && $is_writable), + '#default_value' => $config->get('preprocess_css'), '#disabled' => $disabled, ); $form['bandwidth_optimization']['preprocess_js'] = array( '#type' => 'checkbox', '#title' => t('Aggregate JavaScript files.'), - '#default_value' => intval(variable_get('preprocess_js', 0) && $is_writable), + '#default_value' => $config->get('preprocess_js'), '#disabled' => $disabled, ); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save configuration'), + ); + $form['#submit'][] = 'drupal_clear_css_cache'; $form['#submit'][] = 'drupal_clear_js_cache'; // This form allows page compression settings to be changed, which can // invalidate the page cache, so it needs to be cleared on form submit. $form['#submit'][] = 'system_clear_page_cache_submit'; + $form['#submit'][] = 'system_performance_settings_submit'; - return system_settings_form($form); + return $form; +} + +/** + * Form submission handler for system_performance_settings(). + * + * @ingroup forms + */ +function system_performance_settings_submit($form, &$form_state) { + $config = config('system.performance'); + $config->set('cache', $form_state['values']['cache']); + $config->set('cache_lifetime', $form_state['values']['cache_lifetime']); + $config->set('page_cache_maximum_age', $form_state['values']['page_cache_maximum_age']); + $config->set('page_compression', $form_state['values']['page_compression']); + $config->set('preprocess_css', $form_state['values']['preprocess_css']); + $config->set('preprocess_js', $form_state['values']['preprocess_js']); + $config->save(); } /** diff --git a/core/modules/system/system.install b/core/modules/system/system.install index d8112e6..8c34a1d 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -695,6 +695,28 @@ function system_schema() { $schema['cache_path'] = $schema['cache']; $schema['cache_path']['description'] = 'Cache table for path alias lookup.'; + $schema['config'] = array( + 'description' => 'Default active store for the configuration system.', + 'fields' => array( + 'name' => array( + 'description' => 'The identifier for the configuration entry, such as module.example (the name of the file, minus .json.php).', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'data' => array( + 'description' => 'The raw JSON data for this configuration entry.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + 'translatable' => TRUE, + ), + ), + 'primary key' => array('name'), + ); + + $schema['date_format_type'] = array( 'description' => 'Stores configured date format types.', 'fields' => array( @@ -1666,6 +1688,36 @@ function system_update_8002() { } /** + * Adds {config} table for new Configuration system. + */ +function system_update_8003() { + // @todo Temporary. + if (db_table_exists('config')) { + db_drop_table('config'); + } + db_create_table('config', array( + 'description' => 'Default active store for the configuration system.', + 'fields' => array( + 'name' => array( + 'description' => 'The identifier for the configuration entry, such as module.example (the name of the file, minus .json.php).', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'data' => array( + 'description' => 'The raw JSON data for this configuration entry.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + 'translatable' => TRUE, + ), + ), + 'primary key' => array('name'), + )); +} + +/** * @} End of "defgroup updates-7.x-to-8.x" * The next series of updates should start at 9000. */ diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 332e559..750ef17 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2764,12 +2764,27 @@ function system_settings_form_submit($form, &$form_state) { // Exclude unnecessary elements. form_state_values_clean($form_state); + $config_objects = array(); foreach ($form_state['values'] as $key => $value) { + if (isset($form_state['config'][$key])) { + $config_name = $form_state['config'][$key]['name']; + $config_key = $form_state['config'][$key]['path']; + if (empty($config_objects[$config_name])) { + $config_objects[$config_name] = config($config_name); + } + if (!empty($config_objects[$config_name])) { + $config_objects[$config_name]->set($config_key, $value); + continue; + } + } if (is_array($value) && isset($form_state['values']['array_filter'])) { $value = array_keys(array_filter($value)); } variable_set($key, $value); } + foreach ($config_objects as $config) { + $config->save(); + } drupal_set_message(t('The configuration options have been saved.')); } diff --git a/core/modules/system/system.test b/core/modules/system/system.test index 3a9adcb..cd309c8 100644 --- a/core/modules/system/system.test +++ b/core/modules/system/system.test @@ -1587,8 +1587,6 @@ class SystemMainContentFallback extends DrupalWebTestCase { * Tests for the theme interface functionality. */ class SystemThemeFunctionalTest extends DrupalWebTestCase { - protected $profile = 'testing'; - public static function getInfo() { return array( 'name' => 'Theme interface functionality', @@ -1598,9 +1596,7 @@ class SystemThemeFunctionalTest extends DrupalWebTestCase { } function setUp() { - parent::setUp(array('node', 'block')); - - $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + parent::setUp(); $this->admin_user = $this->drupalCreateUser(array('access administration pages', 'view the administration theme', 'administer themes', 'bypass node access', 'administer blocks')); $this->drupalLogin($this->admin_user); @@ -1613,127 +1609,26 @@ class SystemThemeFunctionalTest extends DrupalWebTestCase { function testThemeSettings() { // Specify a filesystem path to be used for the logo. $file = current($this->drupalGetTestFiles('image')); - $file_relative = strtr($file->uri, array('public:/' => variable_get('file_public_path', conf_path() . '/files'))); - $default_theme_path = 'core/themes/stark'; - - $supported_paths = array( - // Raw stream wrapper URI. - $file->uri => array( - 'form' => file_uri_target($file->uri), - 'src' => file_create_url($file->uri), - ), - // Relative path within the public filesystem. - file_uri_target($file->uri) => array( - 'form' => file_uri_target($file->uri), - 'src' => file_create_url($file->uri), - ), - // Relative path to a public file. - $file_relative => array( - 'form' => $file_relative, - 'src' => file_create_url($file->uri), - ), - // Relative path to an arbitrary file. - 'core/misc/druplicon.png' => array( - 'form' => 'core/misc/druplicon.png', - 'src' => $GLOBALS['base_url'] . '/' . 'core/misc/druplicon.png', - ), - // Relative path to a file in a theme. - $default_theme_path . '/logo.png' => array( - 'form' => $default_theme_path . '/logo.png', - 'src' => $GLOBALS['base_url'] . '/' . $default_theme_path . '/logo.png', - ), - ); - foreach ($supported_paths as $input => $expected) { - $edit = array( - 'default_logo' => FALSE, - 'logo_path' => $input, - ); - $this->drupalPost('admin/appearance/settings', $edit, t('Save configuration')); - $this->assertNoText('The custom logo path is invalid.'); - $this->assertFieldByName('logo_path', $expected['form']); - - // Verify logo path examples. - $elements = $this->xpath('//div[contains(@class, :item)]/div[@class=:description]/code', array( - ':item' => 'form-item-logo-path', - ':description' => 'description', - )); - // Expected default values (if all else fails). - $implicit_public_file = 'logo.png'; - $explicit_file = 'public://logo.png'; - $local_file = $default_theme_path . '/logo.png'; - // Adjust for fully qualified stream wrapper URI in public filesystem. - if (file_uri_scheme($input) == 'public') { - $implicit_public_file = file_uri_target($input); - $explicit_file = $input; - $local_file = strtr($input, array('public:/' => variable_get('file_public_path', conf_path() . '/files'))); - } - // Adjust for fully qualified stream wrapper URI elsewhere. - elseif (file_uri_scheme($input) !== FALSE) { - $explicit_file = $input; - } - // Adjust for relative path within public filesystem. - elseif ($input == file_uri_target($file->uri)) { - $implicit_public_file = $input; - $explicit_file = 'public://' . $input; - $local_file = variable_get('file_public_path', conf_path() . '/files') . '/' . $input; - } - $this->assertEqual((string) $elements[0], $implicit_public_file); - $this->assertEqual((string) $elements[1], $explicit_file); - $this->assertEqual((string) $elements[2], $local_file); - - // Verify the actual 'src' attribute of the logo being output. - $this->drupalGet(''); - $elements = $this->xpath('//*[@id=:id]/img', array(':id' => 'logo')); - $this->assertEqual((string) $elements[0]['src'], $expected['src']); - } - - $unsupported_paths = array( - // Stream wrapper URI to non-existing file. - 'public://whatever.png', - 'private://whatever.png', - 'temporary://whatever.png', - // Bogus stream wrapper URIs. - 'public:/whatever.png', - '://whatever.png', - ':whatever.png', - 'public://', - // Relative path within the public filesystem to non-existing file. - 'whatever.png', - // Relative path to non-existing file in public filesystem. - variable_get('file_public_path', conf_path() . '/files') . '/whatever.png', - // Semi-absolute path to non-existing file in public filesystem. - '/' . variable_get('file_public_path', conf_path() . '/files') . '/whatever.png', - // Relative path to arbitrary non-existing file. - 'core/misc/whatever.png', - // Semi-absolute path to arbitrary non-existing file. - '/core/misc/whatever.png', - // Absolute paths to any local file (even if it exists). - drupal_realpath($file->uri), + $fullpath = drupal_realpath($file->uri); + $edit = array( + 'default_logo' => FALSE, + 'logo_path' => $fullpath, ); - $this->drupalGet('admin/appearance/settings'); - foreach ($unsupported_paths as $path) { - $edit = array( - 'default_logo' => FALSE, - 'logo_path' => $path, - ); - $this->drupalPost(NULL, $edit, t('Save configuration')); - $this->assertText('The custom logo path is invalid.'); - } + $this->drupalPost('admin/appearance/settings', $edit, t('Save configuration')); + $this->drupalGet('node'); + $this->assertRaw($fullpath, t('Logo path successfully changed.')); // Upload a file to use for the logo. + $file = current($this->drupalGetTestFiles('image')); $edit = array( 'default_logo' => FALSE, 'logo_path' => '', 'files[logo_upload]' => drupal_realpath($file->uri), ); - $this->drupalPost('admin/appearance/settings', $edit, t('Save configuration')); - - $fields = $this->xpath($this->constructFieldXpath('name', 'logo_path')); - $uploaded_filename = 'public://' . $fields[0]['value']; - - $this->drupalGet(''); - $elements = $this->xpath('//*[@id=:id]/img', array(':id' => 'logo')); - $this->assertEqual($elements[0]['src'], file_create_url($uploaded_filename)); + $options = array(); + $this->drupalPost('admin/appearance/settings', $edit, t('Save configuration'), $options); + $this->drupalGet('node'); + $this->assertRaw($file->name, t('Logo file successfully uploaded.')); } /** @@ -1792,20 +1687,20 @@ class SystemThemeFunctionalTest extends DrupalWebTestCase { * Test switching the default theme. */ function testSwitchDefaultTheme() { - // Enable Bartik and set it as the default theme. - theme_enable(array('bartik')); + // Enable "stark" and set it as the default theme. + theme_enable(array('stark')); $this->drupalGet('admin/appearance'); - $this->clickLink(t('Set default')); - $this->assertEqual(variable_get('theme_default', ''), 'bartik'); + $this->clickLink(t('Set default'), 1); + $this->assertTrue(variable_get('theme_default', '') == 'stark', t('Site default theme switched successfully.')); // Test the default theme on the secondary links (blocks admin page). $this->drupalGet('admin/structure/block'); - $this->assertText('Bartik(' . t('active tab') . ')', t('Default local task on blocks admin page is the default theme.')); - // Switch back to Stark and test again to test that the menu cache is cleared. + $this->assertText('Stark(' . t('active tab') . ')', t('Default local task on blocks admin page is the default theme.')); + // Switch back to Bartik and test again to test that the menu cache is cleared. $this->drupalGet('admin/appearance'); $this->clickLink(t('Set default'), 0); $this->drupalGet('admin/structure/block'); - $this->assertText('Stark(' . t('active tab') . ')', t('Default local task on blocks admin page has changed.')); + $this->assertText('Bartik(' . t('active tab') . ')', t('Default local task on blocks admin page has changed.')); } } diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 83548f4..b24ff5d 100755 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -237,6 +237,28 @@ $update_free_access = FALSE; $drupal_hash_salt = ''; /** + * Location of the site configuration files. + * + * By default, Drupal configuration files are stored in a randomly named + * directory under the default public files path. On install the + * named directory is created in the default files directory. For enhanced + * security, you may set this variable to a location outside your docroot. + * + * @todo Flesh this out, provide more details, etc. + * + * Example: + * $config_directory_name = '/some/directory/outside/webroot'; + */ +$config_directory_name = ''; + +/** + * Configuration signature key. + * + * Drupal configuration files are signed using this key. + */ +$config_signature_key = ''; + +/** * Base URL (optional). * * If Drupal is generating incorrect URLs on your site, which could