diff --git a/core/core.services.yml b/core/core.services.yml index 1ee07fe..936dce1 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1278,6 +1278,18 @@ services: class: Drupal\Core\StreamWrapper\TemporaryStream tags: - { name: stream_wrapper, scheme: temporary } + stream_wrapper.module: + class: Drupal\Core\StreamWrapper\ModuleStream + tags: + - { name: stream_wrapper, scheme: module } + stream_wrapper.theme: + class: Drupal\Core\StreamWrapper\ThemeStream + tags: + - { name: stream_wrapper, scheme: theme } + stream_wrapper.profile: + class: Drupal\Core\StreamWrapper\ProfileStream + tags: + - { name: stream_wrapper, scheme: profile } kernel_destruct_subscriber: class: Drupal\Core\EventSubscriber\KernelDestructionSubscriber tags: diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index ec12097..b7f68ff 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -243,14 +243,18 @@ function drupal_get_filename($type, $name, $filename = NULL) { /** * Returns the path to a system item (module, theme, etc.). * - * @param $type + * This function should only be used when including a file containing PHP code; + * the 'module://', 'profile://' and 'theme://' stream wrappers should be used + * for other use cases. + * + * @param string $type * The type of the item; one of 'core', 'profile', 'module', 'theme', or * 'theme_engine'. * @param $name * The name of the item for which the path is requested. Ignored for * $type 'core'. * - * @return + * @return string * The path to the requested item or an empty string if the item is not found. */ function drupal_get_path($type, $name) { diff --git a/core/lib/Drupal/Core/StreamWrapper/ExtensionStreamBase.php b/core/lib/Drupal/Core/StreamWrapper/ExtensionStreamBase.php new file mode 100644 index 0000000..ae53a33 --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/ExtensionStreamBase.php @@ -0,0 +1,101 @@ +uri, 2); + return strtok($uri_parts[1], '/'); + } + + /** + * {@inheritdoc} + */ + protected function getTarget($uri = NULL) { + if ($target = strstr(parent::getTarget($uri), '/')) { + return trim($target, '/'); + } + return ''; + } + + /** + * {@inheritdoc} + */ + public function getExternalUrl() { + $dir = $this->getDirectoryPath(); + if (empty($dir)) { + throw new \RuntimeException("Extension directory for {$this->uri} does not exist."); + } + $path = rtrim(base_path() . $dir . '/' . $this->getTarget(), '/'); + return $this->getRequestStack()->getCurrentRequest()->getUriForPath($path); + } + + /** + * {@inheritdoc} + */ + public function dirname($uri = NULL) { + if (!isset($uri)) { + $uri = $this->uri; + } + else { + $this->uri = $uri; + } + + list($scheme) = explode('://', $uri, 2); + $dirname = dirname($this->getTarget($uri)); + $dirname = $dirname !== '.' ? rtrim("/$dirname", '/') : ''; + + return "$scheme://{$this->getOwnerName()}{$dirname}"; + } + + /** + * Returns the request stack object. + * + * @return \Symfony\Component\HttpFoundation\RequestStack + * The request stack object. + */ + protected function getRequestStack() { + if (!isset($this->requestStack)) { + $this->requestStack = \Drupal::service('request_stack'); + } + return $this->requestStack; + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php index 343b461..8ee6d24 100644 --- a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php @@ -10,32 +10,12 @@ * "sites/default/files/example.txt" and then PHP filesystem functions are * invoked. * - * Drupal\Core\StreamWrapper\LocalStream implementations need to implement at least the - * getDirectoryPath() and getExternalUrl() methods. + * \Drupal\Core\StreamWrapper\LocalStream implementations need to implement at + * least the getDirectoryPath() and getExternalUrl() methods. */ -abstract class LocalStream implements StreamWrapperInterface { - /** - * Stream context resource. - * - * @var resource - */ - public $context; +abstract class LocalStream extends StreamWrapperBase { - /** - * A generic resource handle. - * - * @var resource - */ - public $handle = NULL; - - /** - * Instance URI (stream). - * - * A stream is referenced as "scheme://target". - * - * @var string - */ - protected $uri; + use LocalStreamTrait; /** * {@inheritdoc} @@ -47,58 +27,15 @@ public static function getType() { /** * Gets the path that the wrapper is responsible for. * - * @todo Review this method name in D8 per https://www.drupal.org/node/701358. - * * @return string * String specifying the path. */ - abstract function getDirectoryPath(); - - /** - * {@inheritdoc} - */ - function setUri($uri) { - $this->uri = $uri; - } + protected abstract function getDirectoryPath(); /** * {@inheritdoc} */ - function getUri() { - return $this->uri; - } - - /** - * Returns the local writable target of the resource within the stream. - * - * This function should be used in place of calls to realpath() or similar - * functions when attempting to determine the location of a file. While - * functions like realpath() may return the location of a read-only file, this - * method may return a URI or path suitable for writing that is completely - * separate from the URI used for reading. - * - * @param string $uri - * Optional URI. - * - * @return string|bool - * Returns a string representing a location suitable for writing of a file, - * or FALSE if unable to write to the file such as with read-only streams. - */ - protected function getTarget($uri = NULL) { - if (!isset($uri)) { - $uri = $this->uri; - } - - list(, $target) = explode('://', $uri, 2); - - // Remove erroneous leading or trailing, forward-slashes and backslashes. - return trim($target, '\/'); - } - - /** - * {@inheritdoc} - */ - function realpath() { + public function realpath() { return $this->getLocalPath(); } @@ -137,6 +74,7 @@ protected function getLocalPath($uri = NULL) { $realpath = realpath(dirname($path)) . '/' . drupal_basename($path); } $directory = realpath($this->getDirectoryPath()); + if (!$realpath || !$directory || strpos($realpath, $directory) !== 0) { return FALSE; } @@ -165,7 +103,7 @@ public function stream_open($uri, $mode, $options, &$opened_path) { $path = $this->getLocalPath(); $this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode); - if ((bool) $this->handle && $options & STREAM_USE_PATH) { + if ((bool) $this->handle && ($options & STREAM_USE_PATH)) { $opened_path = $path; } @@ -395,33 +333,6 @@ public function rename($from_uri, $to_uri) { } /** - * Gets the name of the directory from a given path. - * - * This method is usually accessed through drupal_dirname(), which wraps - * around the PHP dirname() function because it does not support stream - * wrappers. - * - * @param string $uri - * A URI or path. - * - * @return string - * A string containing the directory name. - * - * @see drupal_dirname() - */ - public function dirname($uri = NULL) { - list($scheme) = explode('://', $uri, 2); - $target = $this->getTarget($uri); - $dirname = dirname($target); - - if ($dirname == '.') { - $dirname = ''; - } - - return $scheme . '://' . $dirname; - } - - /** * Support for mkdir(). * * @param string $uri diff --git a/core/lib/Drupal/Core/StreamWrapper/LocalStreamTrait.php b/core/lib/Drupal/Core/StreamWrapper/LocalStreamTrait.php new file mode 100644 index 0000000..141f330 --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/LocalStreamTrait.php @@ -0,0 +1,70 @@ +uri; + } + + list($scheme) = explode('://', $uri, 2); + $dirname = dirname($this->getTarget($uri)); + + return $dirname !== '.' ? "$scheme://$dirname" : "$scheme://"; + } + + /** + * Returns the local writable target of the resource within the stream. + * + * This function should be used in place of calls to realpath() or similar + * functions when attempting to determine the location of a file. While + * functions like realpath() may return the location of a read-only file, this + * method may return a URI or path suitable for writing that is completely + * separate from the URI used for reading. + * + * @param string $uri + * Optional URI. + * + * @return string + * Returns a string representing a location suitable for writing of a file. + * + * @throws \InvalidArgumentException + * If a malformed $uri parameter is passed in. + */ + protected function getTarget($uri = NULL) { + if (!isset($uri)) { + $uri = $this->uri; + } + + $uri_parts = explode('://', $uri, 2); + if (count($uri_parts) === 1) { + // The delimiter ('://') was not found in $uri, malformed $uri passed. + throw new \InvalidArgumentException("Malformed uri parameter passed: $uri"); + } + + // Remove erroneous leading or trailing forward-slashes and backslashes. + return trim($uri_parts[1], '\/'); + } + + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php b/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php new file mode 100644 index 0000000..d88a3c0 --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php @@ -0,0 +1,63 @@ +getModuleHandler()->moduleExists($name)) { + // The module does not exist or is not installed. + throw new \RuntimeException("Module $name does not exist or is not installed"); + } + return $name; + } + + /** + * {@inheritdoc} + */ + protected function getDirectoryPath() { + return $this->getModuleHandler()->getModule($this->getOwnerName())->getPath(); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->t('Module files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('Local files stored under module directory.'); + } + + /** + * Returns the module handler service. + * + * @return \Drupal\Core\Extension\ModuleHandlerInterface + * The module handler service. + */ + protected function getModuleHandler() { + if (!isset($this->moduleHandler)) { + $this->moduleHandler = \Drupal::moduleHandler(); + } + return $this->moduleHandler; + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php b/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php new file mode 100644 index 0000000..36f34c3 --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php @@ -0,0 +1,33 @@ +t('Installed profile files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('Local files stored under installed profile directory.'); + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/ReadOnlyStream.php b/core/lib/Drupal/Core/StreamWrapper/ReadOnlyStream.php index 1b48910..c0e3cd6 100644 --- a/core/lib/Drupal/Core/StreamWrapper/ReadOnlyStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/ReadOnlyStream.php @@ -12,43 +12,7 @@ * Drupal\Core\StreamWrapper\ReadOnlyStream implementations need to implement * all the read-related classes. */ -abstract class ReadOnlyStream implements StreamWrapperInterface { - /** - * Stream context resource. - * - * @var resource - */ - public $context; - - /** - * A generic resource handle. - * - * @var resource - */ - public $handle = NULL; - - /** - * Instance URI (stream). - * - * A stream is referenced as "scheme://target". - * - * @var string - */ - protected $uri; - - /** - * {@inheritdoc} - */ - function setUri($uri) { - $this->uri = $uri; - } - - /** - * {@inheritdoc} - */ - function getUri() { - return $this->uri; - } +abstract class ReadOnlyStream extends StreamWrapperBase { /** * Support for fopen(), file_get_contents(), etc. diff --git a/core/lib/Drupal/Core/StreamWrapper/StreamWrapperBase.php b/core/lib/Drupal/Core/StreamWrapper/StreamWrapperBase.php new file mode 100644 index 0000000..9bea1ad --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/StreamWrapperBase.php @@ -0,0 +1,51 @@ +uri}"); + } + $this->uri = $uri; + } + + /** + * {@inheritdoc} + */ + function getUri() { + return $this->uri; + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php b/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php index 0d38240..5cab825 100644 --- a/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php +++ b/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php @@ -175,7 +175,7 @@ public function realpath(); * An optional URI. * * @return string - * A string containing the directory name, or FALSE if not applicable. + * A string containing the directory name. * * @see drupal_dirname() */ diff --git a/core/lib/Drupal/Core/StreamWrapper/ThemeStream.php b/core/lib/Drupal/Core/StreamWrapper/ThemeStream.php new file mode 100644 index 0000000..baf65ac --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/ThemeStream.php @@ -0,0 +1,63 @@ +getThemeHandler()->themeExists($name)) { + // The theme does not exist or is not installed. + throw new \RuntimeException("Theme $name does not exist or is not installed"); + } + return $name; + } + + /** + * {@inheritdoc} + */ + protected function getDirectoryPath() { + return $this->getThemeHandler()->getTheme($this->getOwnerName())->getPath(); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->t('Theme files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('Local files stored under theme directory.'); + } + + /** + * Returns the theme handler service. + * + * @return \Drupal\Core\Extension\ThemeHandlerInterface + * The theme handler service. + */ + protected function getThemeHandler() { + if (!isset($this->themeHandler)) { + $this->themeHandler = \Drupal::service('theme_handler'); + } + return $this->themeHandler; + } + +} diff --git a/core/modules/color/color.module b/core/modules/color/color.module index ca0dc3a..698dbfe 100644 --- a/core/modules/color/color.module +++ b/core/modules/color/color.module @@ -399,7 +399,7 @@ function color_scheme_form_submit($form, FormStateInterface $form_state) { // Make sure enough memory is available. if (isset($info['base_image'])) { // Fetch source image dimensions. - $source = drupal_get_path('theme', $theme) . '/' . $info['base_image']; + $source = "theme://$theme/{$info['base_image']}"; list($width, $height) = getimagesize($source); // We need at least a copy of the source and a target buffer of the same diff --git a/core/modules/file/tests/file_test/file_test.dummy.inc b/core/modules/file/tests/file_test/file_test.dummy.inc new file mode 100644 index 0000000..a09fd2e --- /dev/null +++ b/core/modules/file/tests/file_test/file_test.dummy.inc @@ -0,0 +1 @@ +Dummy file used by SystemStreamTest.php. diff --git a/core/modules/search/src/Tests/SearchSimplifyTest.php b/core/modules/search/src/Tests/SearchSimplifyTest.php index bdb96c9..1e32704 100644 --- a/core/modules/search/src/Tests/SearchSimplifyTest.php +++ b/core/modules/search/src/Tests/SearchSimplifyTest.php @@ -20,7 +20,7 @@ function testSearchSimplifyUnicode() { // their own lines). So the even-numbered lines should simplify to nothing, // and the odd-numbered lines we need to split into shorter chunks and // verify that simplification doesn't lose any characters. - $input = file_get_contents(\Drupal::root() . '/core/modules/search/tests/UnicodeTest.txt'); + $input = file_get_contents('module://search/tests/UnicodeTest.txt'); $basestrings = explode(chr(10), $input); $strings = array(); foreach ($basestrings as $key => $string) { diff --git a/core/modules/system/tests/src/Kernel/File/ExtensionStreamTest.php b/core/modules/system/tests/src/Kernel/File/ExtensionStreamTest.php new file mode 100644 index 0000000..6b86773 --- /dev/null +++ b/core/modules/system/tests/src/Kernel/File/ExtensionStreamTest.php @@ -0,0 +1,271 @@ +baseUrl = $this->container->get('request_stack')->getCurrentRequest()->getUriForPath(base_path()); + + /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */ + $stream_wrapper_manager = $this->container->get('stream_wrapper_manager'); + // Get stream wrapper instances. + foreach (['module', 'theme', 'profile'] as $scheme) { + $this->streamWrappers[$scheme] = $stream_wrapper_manager->getViaScheme($scheme); + } + + /** @var \Drupal\Core\State\StateInterface $state */ + $state = $this->container->get('state'); + + // Set 'minimal' as installed profile for the purposes of this test. + $system_module_files = $state->get('system.module.files', []); + $system_module_files += ['minimal' => 'core/profiles/minimal/minimal.info.yml']; + $state->set('system.module.files', $system_module_files); + // Add default profile for the purposes of this test. + new Settings(Settings::getAll() + ['install_profile' => 'minimal']); + $this->config('core.extension')->set('module.minimal', 0)->save(); + $this->container->get('module_handler')->addProfile('minimal', 'core/profiles/minimal'); + + /** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */ + $theme_installer = $this->container->get('theme_installer'); + // Install Bartik and Seven themes. + $theme_installer->install(['bartik', 'seven']); + } + + /** + * Tests invalid stream uris. + * + * @param string $uri + * The URI being tested. + * + * @dataProvider providerInvalidUris + */ + public function testInvalidStreamUri($uri) { + $message = "\\InvalidArgumentException thrown on invalid uri $uri."; + try { + $this->streamWrappers['module']->dirname($uri); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->assertSame($e->getMessage(), "Malformed uri parameter passed: $uri", $message); + } + } + + /** + * Provides test cases for testInvalidStreamUri() + * + * @return array[] + * A list of urls to test. + */ + public function providerInvalidUris() { + return [ + ['invalid/uri'], + ['invalid_uri'], + ['module/invalid/uri'], + ['module/invalid_uri'], + ['module:invalid_uri'], + ['module::/invalid/uri'], + ['module::/invalid_uri'], + ['module//:invalid/uri'], + ['module//invalid_uri'], + ['module//invalid/uri'], + ]; + } + + /** + * Tests call of ::dirname() without setting a URI first. + */ + public function testDirnameAsParameter() { + $this->assertEquals('module://system', $this->streamWrappers['module']->dirname('module://system/system.admin.css')); + } + + /** + * Test the extension stream wrapper methods. + * + * @param string $uri + * The uri to be tested. + * @param string|\RuntimeException|\InvalidArgumentException $dirname + * The expectation for dirname() method. + * @param string|\RuntimeException|\InvalidArgumentException $realpath + * The expectation for realpath() method. + * @param string|\RuntimeException|\InvalidArgumentException $getExternalUrl + * The expectation for getExternalUrl() method. + * + * @dataProvider providerStreamWrapperMethods + */ + public function testStreamWrapperMethods($uri, $dirname, $realpath, $getExternalUrl) { + // Prefix realpath() expected value with Drupal root directory. + $realpath = is_string($realpath) ? DRUPAL_ROOT . $realpath : $realpath; + // Prefix getExternalUrl() expected value with base url. + $getExternalUrl = is_string($getExternalUrl) ? "{$this->baseUrl}$getExternalUrl" : $getExternalUrl; + $case = compact('dirname', 'realpath', 'getExternalUrl'); + + foreach ($case as $method => $expected) { + list($scheme, ) = explode('://', $uri); + $this->streamWrappers[$scheme]->setUri($uri); + if ($expected instanceof \InvalidArgumentException || $expected instanceof \RuntimeException) { + /** @var \Exception $expected */ + $message = sprintf('Exception thrown: %s("%s").', get_class($expected), $expected->getMessage()); + try { + $this->streamWrappers[$scheme]->$method(); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->assertSame($expected->getMessage(), $e->getMessage(), $message); + } + catch (\RuntimeException $e) { + $this->assertSame($expected->getMessage(), $e->getMessage(), $message); + } + } + elseif (is_string($expected)) { + $this->assertSame($expected, $this->streamWrappers[$scheme]->$method()); + } + } + } + + /** + * Provides test cases for testStreamWrapperMethods(). + * + * @return array[] + * A list of test cases. Each case consists of the following items: + * - The uri to be tested. + * - The result or the exception when running dirname() method. + * - The result or the exception when running realpath() method. The value + * is prefixed later, in the test method, with the Drupal root directory. + * - The result or the exception when running getExternalUrl() method. The + * value is prefixed later, in the test method, with the base url. + */ + public function providerStreamWrapperMethods() { + return [ + // Cases for module:// stream wrapper. + [ + 'module://system', + 'module://system', + '/core/modules/system', + 'core/modules/system', + ], + [ + 'module://system/css/system.admin.css', + 'module://system/css', + '/core/modules/system/css/system.admin.css', + 'core/modules/system/css/system.admin.css', + ], + [ + 'module://file_module_test/file_module_test.dummy.inc', + 'module://file_module_test', + '/core/modules/file/tests/file_module_test/file_module_test.dummy.inc', + 'core/modules/file/tests/file_module_test/file_module_test.dummy.inc', + ], + [ + 'module://file_module_test/src/file_module_test.dummy.inc', + 'module://file_module_test/src', + '/core/modules/file/tests/file_module_test/src/file_module_test.dummy.inc', + 'core/modules/file/tests/file_module_test/src/file_module_test.dummy.inc', + ], + [ + 'module://ckeditor/ckeditor.info.yml', + new \RuntimeException('Module ckeditor does not exist or is not installed'), + new \RuntimeException('Module ckeditor does not exist or is not installed'), + new \RuntimeException('Module ckeditor does not exist or is not installed'), + ], + [ + 'module://foo_bar/foo.bar.js', + new \RuntimeException('Module foo_bar does not exist or is not installed'), + new \RuntimeException('Module foo_bar does not exist or is not installed'), + new \RuntimeException('Module foo_bar does not exist or is not installed'), + ], + // Cases for theme:// stream wrapper. + [ + 'theme://seven', + 'theme://seven', + '/core/themes/seven', + 'core/themes/seven', + ], + [ + 'theme://seven/style.css', + 'theme://seven', + '/core/themes/seven/style.css', + 'core/themes/seven/style.css', + ], + [ + 'theme://bartik/color/preview.js', + 'theme://bartik/color', + '/core/themes/bartik/color/preview.js', + 'core/themes/bartik/color/preview.js', + ], + [ + 'theme://fifteen/screenshot.png', + new \RuntimeException('Theme fifteen does not exist or is not installed'), + new \RuntimeException('Theme fifteen does not exist or is not installed'), + new \RuntimeException('Theme fifteen does not exist or is not installed'), + ], + [ + 'theme://stark/stark.info.yml', + new \RuntimeException('Theme stark does not exist or is not installed'), + new \RuntimeException('Theme stark does not exist or is not installed'), + new \RuntimeException('Theme stark does not exist or is not installed'), + ], + // Cases for profile:// stream wrapper. + [ + 'profile://', + 'profile://', + '/core/profiles/minimal', + 'core/profiles/minimal', + ], + [ + 'profile://config/install/block.block.stark_login.yml', + 'profile://config/install', + '/core/profiles/minimal/config/install/block.block.stark_login.yml', + 'core/profiles/minimal/config/install/block.block.stark_login.yml', + ], + [ + 'profile://config/install/node.type.article.yml', + 'profile://config/install', + '/core/profiles/minimal/config/install/node.type.article.yml', + 'core/profiles/minimal/config/install/node.type.article.yml', + ], + [ + 'profile://minimal.info.yml', + 'profile://', + '/core/profiles/minimal/minimal.info.yml', + 'core/profiles/minimal/minimal.info.yml', + ], + ]; + } + +}