diff --git a/.htaccess b/.htaccess index a69bdd4..398f960 100644 --- a/.htaccess +++ b/.htaccess @@ -3,7 +3,7 @@ # # Protect files and directories from prying eyes. - + Order allow,deny diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index ab06260..1a06544 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -239,6 +239,9 @@ const REGISTRY_WRITE_LOOKUP_CACHE = 2; */ const DRUPAL_PHP_FUNCTION_PATTERN = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; +// Will go away when we've converted to PSR-0 +require_once DRUPAL_ROOT . '/core/includes/config.inc'; + /** * Provides a caching wrapper to be used in place of large array structures. * @@ -766,7 +769,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 +1345,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 +1374,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 +2340,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 a4fcf51..c4a42b4 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -2596,7 +2596,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 { @@ -5179,7 +5180,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..7f59ba3 --- /dev/null +++ b/core/includes/config.inc @@ -0,0 +1,252 @@ + $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_xml2array($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()); + 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/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..b44f154 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -986,7 +986,31 @@ 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? + }; // 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..7007837 --- /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: + * + * + * baz + * + * + * 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. + * + * Note that unlike variable_get(), the config system does not maintain + * type information, and everything saved into it gets cast to a string. + * In most cases this is not an issue, however it can cause tricky problems + * with booleans, which get cast to "1" (TRUE) or "" (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) { + $parts = explode('.', $key); + if (count($parts) == 1) { + $this->data[$key] = $value; + } + else { + drupal_array_set_nested_value($this->data, $parts, $value); + } + } + + /** + * Unsets value in this config object. + * + * @param $key + * @todo + */ + 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..f87a19d --- /dev/null +++ b/core/lib/Drupal/Core/Config/DrupalConfigVerifiedStorage.php @@ -0,0 +1,83 @@ +name = $name; + } + + /** + * @todo + * + * @return + * @todo + */ + protected function signedFileStorage() { + return new SignedFileStorage($this->name); + } + + /** + * Implements DrupalConfigVerifiedStorageInterface::copyToFile(). + */ + public function copyToFile() { + return $this->signedFileStorage()->write($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->copyToFile(); + } + + /** + * 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..b5eae3a --- /dev/null +++ b/core/lib/Drupal/Core/Config/DrupalConfigVerifiedStorageInterface.php @@ -0,0 +1,76 @@ + $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..f0f946a --- /dev/null +++ b/core/lib/Drupal/Core/Config/SignedFileStorage.php @@ -0,0 +1,146 @@ +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..96987e8 --- /dev/null +++ b/core/modules/config/config.info @@ -0,0 +1,6 @@ +name = Configuration manager +version = VERSION +core = 8.x +files[] = config.module +files[] = config.test +package = Core diff --git a/core/modules/config/config.module b/core/modules/config/config.module new file mode 100644 index 0000000..a4abe2d --- /dev/null +++ b/core/modules/config/config.module @@ -0,0 +1,2 @@ + 'Secure file tests', + 'description' => 'Tests the saving of secure 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 FileContentsTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Config file content tests', + 'description' => 'Tests the reading and writing of config settings.', + 'group' => 'Configuration', + ); + } + + /** + * Tests that a simple setting can be written and read. + */ + public function testReadWriteConfig() { + $config = config('foo.bar'); + $config->set('foo', 'bar'); + $config->save(); + $this->assertEqual('bar', config('foo.bar')->get('foo'), 'Content retrived from written config data.'); + } +} 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..b721407 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,42 +154,44 @@ 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'], + ); + $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. + if (isset($form_state['values']['name']) && $style['name'] != $form_state['values']['name']) { + image_style_delete($style); + $style['name'] = $form_state['values']['name']; + } + image_style_save($style); if ($form_state['values']['op'] == t('Update style')) { drupal_set_message(t('Changes to the style have been saved.')); @@ -273,7 +241,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 +292,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 +360,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 +395,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 +591,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 +628,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 +661,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.module b/core/modules/image/image.module index 4d7b967..6301376 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) { @@ -493,41 +445,19 @@ function image_styles() { 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 the styles we have configured. + foreach (config_get_signed_file_storage_names_with_prefix('image.styles') as $config_name) { + $style = array(); + $config = config($config_name); + $style['name'] = $config->get('name'); + $style['effects'] = array(); + foreach ($config->get('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; - } - drupal_alter('image_styles', $styles); cache()->set('image_styles', $styles); } @@ -541,41 +471,19 @@ 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) { +function image_style_load($name = NULL) { $styles = image_styles(); // If retrieving by name. if (isset($name) && isset($styles[$name])) { - $style = $styles[$name]; - } - - // 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; - } - } - } - - // 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; + return $styles[$name]; } // Otherwise the style was not found. @@ -591,19 +499,16 @@ function image_style_load($name = NULL, $isid = NULL, $include = NULL) { * An image style array. In the case of a new style, 'isid' will be populated. */ 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 ($style['effects']) { + $config->set('effects', $style['effects']); } else { - drupal_write_record('image_styles', $style); - $style['is_new'] = TRUE; + $config->set('effects', array()); } + $config->save(); + $style['is_new'] = TRUE; // Let other modules update as necessary on save. module_invoke_all('image_style_save', $style); @@ -628,8 +533,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']; @@ -876,15 +781,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 +830,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 +882,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 +893,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; } @@ -1078,8 +933,8 @@ function image_effects() { /** * Load a single image effect. * - * @param $ieid - * The image effect ID. + * @param $name + * The image effect name. * @param $style_name * The image style name. * @param $include @@ -1098,9 +953,9 @@ 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($name, $style_name) { + if (($style = image_style_load($style_name)) && isset($style['effects'][$name])) { + return $style['effects'][$name]; } return FALSE; } @@ -1108,19 +963,37 @@ 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) { +function image_effect_save($style_name, $effect) { + $config = config('image.styles.' . $style_name); + if (!empty($effect['ieid'])) { - drupal_write_record('image_effects', $effect, 'ieid'); + $old_effect = $config->get('effects.' . $effect['ieid']); + foreach ($old_effect as $key => $value) { + $old_effect[$key] = $effect[$key]; + $config->set('effects.' . $effect['ieid'], $old_effect); + } } else { - drupal_write_record('image_effects', $effect); + // 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. + $machine_name = $effect['name']; + + foreach ($effect['data'] as $key => $value) { + $machine_name .= '_' . $value; + } + $effect['ieid'] = $machine_name; + $config->set('effects.' . $machine_name, $effect); } - $style = image_style_load(NULL, $effect['isid']); + $config->save(); + $style = image_style_load(NULL, $style_name); image_style_flush($style); return $effect; } @@ -1128,12 +1001,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); } @@ -1159,7 +1036,7 @@ function image_effect_apply($image, $effect) { * @param $variables * An associative array containing: * - style_name: The name of the style to be used to alter the original image. - * - uri: The path of the image file relative to the Drupal files directory. + * - path: The path of the image file relative to the Drupal files directory. * This function does not work with images outside the files directory nor * with remotely hosted images. * - width: The width of the source image (if known). @@ -1184,7 +1061,7 @@ function theme_image_style($variables) { $variables['height'] = $dimensions['height']; // Determine the url for the styled image. - $variables['uri'] = image_style_url($variables['style_name'], $variables['uri']); + $variables['path'] = image_style_url($variables['style_name'], $variables['path']); return theme('image', $variables); } diff --git a/core/modules/poll/poll.test b/core/modules/poll/poll.test index ea8b341..26f2cfc 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 d6f19c7..6a7c580 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 846f6d3..cca23ba 100644 --- a/core/modules/simpletest/tests/session.test +++ b/core/modules/simpletest/tests/session.test @@ -139,7 +139,10 @@ 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); @@ -455,7 +458,7 @@ class SessionHttpsTestCase extends DrupalWebTestCase { } } - // Test that session data saved before login is not available using the + // Test that session data saved before login is not available using the // pre-login anonymous cookie. $this->cookies = array(); $this->drupalGet('session-test/get', array('Cookie: ' . $anonymous_cookie)); 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 00c4c41..abe1577 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -1617,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', @@ -1638,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'); @@ -1650,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://'; @@ -1676,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 7a275e8..b180165 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -690,6 +690,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( @@ -1661,6 +1683,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/translation/translation.module b/core/modules/translation/translation.module index 20f456c..da719aa 100644 --- a/core/modules/translation/translation.module +++ b/core/modules/translation/translation.module @@ -36,7 +36,7 @@ function translation_help($path, $arg) { $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Configuring content types for translation') . '
'; - $output .= '
' . t('To configure a particular content type for translation, visit the Content types page, and click the edit link for the content type. In the Publishing options section, select Enabled, with translation under Multilingual support.', array('@content-types' => url('admin/structure/types'))) . '
'; + $output .= '
' . t('To configure a particular content type for translation, visit the Content types page, and click the edit link for the content type. In the Publishing options section, select Enabled, with translation under Multilingual support.', array('@content-types' => url('admin/structure/types'))) . '
'; $output .= '
' . t('Assigning a language to content') . '
'; $output .= '
' . t('Use the Language drop down to select the appropriate language when creating or editing content.') . '
'; $output .= '
' . t('Translating content') . '
'; 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