diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 95adc95..1499b6d 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -3516,3 +3516,78 @@ function drupal_check_memory_limit($required, $memory_limit = NULL) { // the operation. return ((!$memory_limit) || ($memory_limit == -1) || (parse_size($memory_limit) >= parse_size($required))); } + +/** + * Read a file from Drupal's PHP storage. + * + * @param string $filename + * The name of the file to read. Can be a relative path. + * @param string $type + * A string that describes the contents of the file. e.g. Twig, DIC + * + * @return + * No return but requested code is included and executable. + */ +function drupal_php_read($filename, $type = 'default') { + include_once _drupal_php_helper($filename, $type); +} + +/** + * Write a file to Drupal's PHP storage. + * + * @param string $filename + * The name of the file to write. Can be a relative path. + * @param string $data + * The contents of the file. + * @param string $type + * A string that describes the contents of the file. e.g. Twig, DIC + */ +function drupal_php_write($filename, $data, $type = 'default') { + file_put_contents(_drupal_php_helper($filename, $type), $data); +} + +/** + * Gets the path to the requested file in Drupal's PHP storage. + * + * @param string $filename + * The name of the file. Can be a relative path. + * @param string $type + * A string that describes the contents of the file. e.g. Twig, DIC + */ +function _drupal_php_helper($filename, $type) { + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['initalized'] = &drupal_static(__FUNCTION__); + } + $initalized = &$drupal_static_fast['initalized']; + + $php_loader = variable_get('php_loader', array()); + // Shortcut the loading process if there's no loader specified. + if (!$php_loader) { + return DRUPAL_ROOT . '/' . $filename; + } + + $index = isset($php_loader[$type]) ? $type : 'default'; + $loader_conf = $php_loader[$index]; + $GLOBALS['conf']['php_loader_current'] = $loader_conf; + if (!isset($initalized[$index])) { + if (isset($loader_conf['init_callback'])) { + $arguments = isset($loader_conf['init_arguments']) ? $loader_conf['init_arguments'] : array(); + call_user_func_array($loader_conf['init_callback'], $arguments); + } + $initalized[$index] = TRUE; + } + if (isset($loader_conf['path_prefix'])) { + $filename = $loader_conf['path_prefix'] . $filename; + } + if (isset($loader_conf['path_prefix_callback'])) { + call_user_func_array($loader_conf['path_prefix_callback'], array(&$filename)); + } + return $filename; +} + +function drupal_stream_wrapper_register($protocol, $wrapper) { + if (array_search($protocol, stream_get_wrappers()) === FALSE) { + stream_wrapper_register($protocol, $wrapper); + } +} diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 32ce838..79dfce2 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1070,6 +1070,20 @@ function install_settings_form_submit($form, &$form_state) { 'value' => $config_directory_name ? $config_directory_name : 'config_' . drupal_hash_base64(drupal_random_bytes(55)), 'required' => TRUE, ); + // This is our basic php loader. + $loader_dir = variable_get('file_public_path', conf_path() . '/files') . '/codegen'; + $settings['conf']['php_loader'] = array( + 'value' => array( + 'default' => array( + 'init_callback' => 'stream_filter_register', + 'init_arguments' => array('drupal.*', 'Drupal\\Core\\PhpLoader\\DrupalStreamFilter'), + 'path_prefix' => $loader_dir . '/', + 'path_prefix_callback' => array('Drupal\\Core\\PhpLoader\\DrupalStreamFilter', 'pathPrepare'), + 'header' => drupal_hash_base64(drupal_random_bytes(55)), + ), + ), + 'required' => TRUE, + ); drupal_rewrite_settings($settings); @@ -1083,6 +1097,9 @@ function install_settings_form_submit($form, &$form_state) { // Bail out using a similar error message as in system_requirements(). throw new Exception(st('The directory %directory could not be created or could not be made writable. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see the online handbook.', array('%directory' => config_get_config_directory(), '@handbook_url' => 'http://drupal.org/server-permissions'))); } + // Now that config has ensured that the files directory is writeable, + // just create the codegen dir. + file_prepare_directory($loader_dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); // 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 diff --git a/core/lib/Drupal/Core/PhpLoader/DatabaseStreamWrapper.php b/core/lib/Drupal/Core/PhpLoader/DatabaseStreamWrapper.php new file mode 100644 index 0000000..02ac967 --- /dev/null +++ b/core/lib/Drupal/Core/PhpLoader/DatabaseStreamWrapper.php @@ -0,0 +1,88 @@ +fields(array('path' => $path_to)) + ->condition('path', $path_from) + ->execute(); + return TRUE; + } + public function stream_eof() { + return $this->eof; + } + public function stream_open($path, $mode, $options, &$opened_path) { + $this->path = $path; + $this->read($path); + $opened_path = $path; + $this->pos = 0; + $this->eof = empty($this->contents); + return TRUE; + } + public function stream_read($count) { + $return = substr($this->contents, $this->pos, $count); + $this->pos += $count; + $this->eof = empty($return); + return $return; + } + public function stream_tell() { + return $this->pos; + } + public function stream_write($data) { + $fields = array( + 'contents' => $data, + 'updated' => REQUEST_TIME, + ); + db_merge('php') + ->key(array('path' => $this->path)) + ->fields($fields) + ->execute(); + return strlen($data); + } + public function unlink($path) { + db_delete('php') + ->condition('path', $path) + ->execute(); + } + public function stream_stat() { + return $this->stat(); + } + public function url_stat($path, $flags) { + $this->read($path); + return $this->stat(); + } + + protected function read($path) { + if (!$contents = db_query('SELECT contents, updated FROM {php} WHERE path = :path', array(':path' => $path))->fetchAssoc()) { + $contents = array('contents' => '', 'updated' => 0); + } + $this->contents = $contents['contents']; + $this->updated = $contents['updated']; + } + + protected function stat() { + $stat = array( + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => strlen($this->contents), + 'atime' => REQUEST_TIME, + 'mtime' => $this->updated, + 'ctime' => $this->updated, + 'blksize' => -1, + 'blocks' => -1, + ); + return array_values($stat) + $stat; + } +} diff --git a/core/lib/Drupal/Core/PhpLoader/DrupalStreamFilter.php b/core/lib/Drupal/Core/PhpLoader/DrupalStreamFilter.php new file mode 100644 index 0000000..fe381d8 --- /dev/null +++ b/core/lib/Drupal/Core/PhpLoader/DrupalStreamFilter.php @@ -0,0 +1,40 @@ +contents .= $bucket->data; + $consumed += $bucket->datalen; + } + $data = ''; + if ($closing && preg_match("|^<\?php(?: //(?'hash'[A-Za-z0-9_-]{43})\n)?(?'code'.*)|s", $this->contents, $matches)) { + $hash = drupal_hash_base64($GLOBALS['conf']['php_loader_current']['header'] . $matches['code']); + switch ($this->filtername) { + case 'drupal.read': + if ($hash == $matches['hash']) { + $data = $this->contents; + } + break; + case 'drupal.write': + $data = 'stream, $data)); + return PSFS_PASS_ON; + } + return PSFS_FEED_ME; + } + public static function pathPrepare(&$filename) { + @mkdir(dirname($filename), 0700, TRUE); + $filename = 'php://filter/read=drupal.read/write=drupal.write/resource=' . $filename; + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/PhpLoader/DatabaseStreamWrapperTest.php b/core/modules/system/lib/Drupal/system/Tests/PhpLoader/DatabaseStreamWrapperTest.php new file mode 100644 index 0000000..7a651c2 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/PhpLoader/DatabaseStreamWrapperTest.php @@ -0,0 +1,30 @@ + 'Database stream wrapper', + 'description' => 'Reads and writes a php file in the database.', + 'group' => 'PHP Loader', + ); + } + protected function conf() { + return array( + 'default' => array( + 'init_callback' => 'drupal_stream_wrapper_register', + 'init_arguments' => array('dbphp', 'Drupal\\Core\\PhpLoader\\DatabaseStreamWrapper'), + 'path_prefix' => 'dbphp://', + ), + ); + } + function testDatabaseStreamWrapper() { + $this->assertIdentical($this->contents, db_query('SELECT contents FROM {php} WHERE path = :path', array(':path' => "dbphp://$this->filename"))->fetchfield()); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/PhpLoader/DrupalStreamFilterTest.php b/core/modules/system/lib/Drupal/system/Tests/PhpLoader/DrupalStreamFilterTest.php new file mode 100644 index 0000000..70f755d --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/PhpLoader/DrupalStreamFilterTest.php @@ -0,0 +1,43 @@ + 'Stream filter', + 'description' => 'Reads and writes a signed php file on the disk.', + 'group' => 'PHP Loader', + ); + } + public function conf() { + $this->loaderDir = variable_get('file_public_path', conf_path() . '/files') . '/codegen/'; + return array( + 'default' => array( + 'init_callback' => 'stream_filter_register', + 'init_arguments' => array('drupal.*', 'Drupal\\Core\\PhpLoader\\DrupalStreamFilter'), + 'path_prefix' => $this->loaderDir, + 'path_prefix_callback' => array('Drupal\\Core\\PhpLoader\\DrupalStreamFilter', 'pathPrepare'), + 'header' => drupal_hash_base64(drupal_random_bytes(55)), + ), + ); + } + + public function testDrupalStreamFilter() { + $contents = file_get_contents($this->loaderDir . $this->filename); + $this->assertIdentical($this->contents, preg_replace('|^<\?php.*\n|', 'loaderDir . $this->filename, ' ', FILE_APPEND); + drupal_php_read($this->filename); + $this->pass('The filter works'); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/PhpLoader/PhpLoaderTest.php b/core/modules/system/lib/Drupal/system/Tests/PhpLoader/PhpLoaderTest.php new file mode 100644 index 0000000..edf3632 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/PhpLoader/PhpLoaderTest.php @@ -0,0 +1,30 @@ +conf(); + $this->filename = $this->randomName() . '.php'; + do { + $random = mt_rand(10000, 100000); + $function = 'test' . $random; + } while (function_exists($function)); + $this->contents = "filename, $this->contents); + drupal_php_read($this->filename); + $this->assertIdentical($function(), $random); + } + abstract protected function conf(); +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 0605a1d..c2c6052 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1374,6 +1374,31 @@ function system_schema() { 'primary key' => array('mlid'), ); + $schema['php'] = array( + 'description' => 'Stores PHP code', + 'fields' => array( + 'path' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The path to the file.', + ), + 'contents' => array( + 'description' => 'The contents of the file.', + 'type' => 'text', + 'not null' => TRUE, + ), + 'updated' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Timestamp when this file was last updated.', + ), + ), + 'primary key' => array('path'), + ); + $schema['queue'] = array( 'description' => 'Stores items in queues.', 'fields' => array(