diff --git a/core/core.services.yml b/core/core.services.yml index afccb31..42cb057 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1242,6 +1242,34 @@ 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.active_theme: + class: Drupal\Core\StreamWrapper\ActiveThemeStream + tags: + - { name: stream_wrapper, scheme: active-theme } + stream_wrapper.admin_theme: + class: Drupal\Core\StreamWrapper\AdminThemeStream + tags: + - { name: stream_wrapper, scheme: admin-theme } + stream_wrapper.default_theme: + class: Drupal\Core\StreamWrapper\DefaultThemeStream + tags: + - { name: stream_wrapper, scheme: default-theme } + stream_wrapper.profile: + class: Drupal\Core\StreamWrapper\ProfileStream + tags: + - { name: stream_wrapper, scheme: profile } + stream_wrapper.installed_profile: + class: Drupal\Core\StreamWrapper\InstalledProfileStream + tags: + - { name: stream_wrapper, scheme: installed-profile } kernel_destruct_subscriber: class: Drupal\Core\EventSubscriber\KernelDestructionSubscriber tags: diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index ce872d4..0240643 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -240,14 +240,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/ActiveThemeStream.php b/core/lib/Drupal/Core/StreamWrapper/ActiveThemeStream.php new file mode 100644 index 0000000..5d2237c --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/ActiveThemeStream.php @@ -0,0 +1,59 @@ +themeNegotiator = \Drupal::service('theme.negotiator'); + $this->routeMatch = \Drupal::service('current_route_match'); + } + + /** + * {@inheritdoc} + */ + protected function getOwnerName() { + return $this->themeNegotiator->determineActiveTheme($this->routeMatch); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return t('Active theme files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return t('Local active theme files.'); + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/AdminThemeStream.php b/core/lib/Drupal/Core/StreamWrapper/AdminThemeStream.php new file mode 100644 index 0000000..a632c17 --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/AdminThemeStream.php @@ -0,0 +1,51 @@ +configFactory = \Drupal::configFactory(); + } + + /** + * {@inheritdoc} + */ + protected function getOwnerName() { + return $this->configFactory->get('system.theme')->get('admin'); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return t('Admin theme files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return t('Local admin theme files.'); + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/DefaultThemeStream.php b/core/lib/Drupal/Core/StreamWrapper/DefaultThemeStream.php new file mode 100644 index 0000000..2189f21 --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/DefaultThemeStream.php @@ -0,0 +1,51 @@ +themeHandler = \Drupal::service('theme_handler'); + } + + /** + * {@inheritdoc} + */ + protected function getOwnerName() { + return $this->themeHandler->getDefault(); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return t('Default theme files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return t('Local default theme files.'); + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/InstalledProfileStream.php b/core/lib/Drupal/Core/StreamWrapper/InstalledProfileStream.php new file mode 100644 index 0000000..0302c38 --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/InstalledProfileStream.php @@ -0,0 +1,44 @@ +getOwnerName())); + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php index 82f4427..db51eda 100644 --- a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php @@ -57,7 +57,7 @@ public static function getType() { * @return string * String specifying the path. */ - abstract function getDirectoryPath(); + protected abstract function getDirectoryPath(); /** * {@inheritdoc} @@ -85,25 +85,31 @@ function getUri() { * @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. + * @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; } - list(, $target) = explode('://', $uri, 2); + $uri_parts = explode('://', $uri, 2); + if (count($uri_parts) === 1) { + // The delimiter ('://') was not found in $uri, malformed $uri passed. + throw new \InvalidArgumentException(sprintf('Malformed uri parameter passed: %s', $uri)); + } - // Remove erroneous leading or trailing, forward-slashes and backslashes. - return trim($target, '\/'); + // Remove erroneous leading or trailing forward-slashes and backslashes. + return trim($uri_parts[1], '\/'); } /** * {@inheritdoc} */ - function realpath() { + public function realpath() { return $this->getLocalPath(); } diff --git a/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php b/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php new file mode 100644 index 0000000..3424c56 --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php @@ -0,0 +1,68 @@ +moduleHandler = \Drupal::moduleHandler(); + } + + /** + * {@inheritdoc} + */ + protected function getOwnerName() { + $name = parent::getOwnerName(); + if ($this->moduleHandler->moduleExists($name)) { + return $name; + } + else { + // The module does not exist or is not installed. + throw new \InvalidArgumentException(sprintf('Module %s does not exist or is not installed', $name)); + } + } + + /** + * {@inheritdoc} + */ + protected function getDirectoryPath() { + return $this->moduleHandler->getModule($this->getOwnerName())->getPath(); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return t('Module files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return t('Local module files.'); + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php b/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php new file mode 100644 index 0000000..539ffc5 --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php @@ -0,0 +1,51 @@ +getOwnerName())); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return t('Profile files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return t('Local profile files.'); + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/PseudoSystemStreamBase.php b/core/lib/Drupal/Core/StreamWrapper/PseudoSystemStreamBase.php new file mode 100644 index 0000000..2ad51be --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/PseudoSystemStreamBase.php @@ -0,0 +1,55 @@ +uri; + } + + list($scheme) = explode('://', $uri, 2); + $target = $this->getTarget($uri); + + $dirname = dirname($target); + + if ($dirname === '.' || $dirname === '\\') { + $dirname = ''; + } + + return "$scheme://" . trim($dirname, '/'); + } + + /** + * {@inheritdoc} + */ + 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(sprintf('Malformed uri parameter passed: %s', $uri)); + } + + $target = trim($uri_parts[1], '\/'); + + return !empty($target) ? "/$target" : ''; + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/PseudoThemeStreamBase.php b/core/lib/Drupal/Core/StreamWrapper/PseudoThemeStreamBase.php new file mode 100644 index 0000000..697784c --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/PseudoThemeStreamBase.php @@ -0,0 +1,23 @@ +getOwnerName())); + } + +} diff --git a/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php b/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php index f13cfb8..2bf1502 100644 --- a/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php +++ b/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php @@ -182,7 +182,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/SystemStream.php b/core/lib/Drupal/Core/StreamWrapper/SystemStream.php new file mode 100644 index 0000000..bf064d2 --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/SystemStream.php @@ -0,0 +1,101 @@ +request = \Drupal::service('request_stack')->getCurrentRequest(); + } + + /** + * {@inheritdoc} + */ + public static function getType() { + return StreamWrapperInterface::LOCAL | StreamWrapperInterface::READ; + } + + /** + * Gets the module, theme, or profile name of the current URI. + * + * This will return the name of the module, theme or profile e.g. + * @code SystemStream::getOwnerName('module://foo') @endcode and @code + * SystemStream::getOwnerName('module://foo/')@endcode will both return @code + * 'foo'@endcode + * + * @return string + * The extension name. + * + * @throws \InvalidArgumentException + */ + protected function getOwnerName() { + $uri_parts = explode('://', $this->uri, 2); + if (count($uri_parts) === 1) { + // The delimiter ('://') was not found in $uri, malformed $uri passed. + throw new \InvalidArgumentException(sprintf('Malformed uri parameter passed: %s', $this->uri)); + } + + // Remove the trailing filename from the path. + $length = strpos($uri_parts[1], '/'); + return ($length === FALSE) ? $uri_parts[1] : substr($uri_parts[1], 0, $length); + } + + /** + * {@inheritdoc} + */ + protected function getTarget($uri = NULL) { + return rtrim(strstr(parent::getTarget($uri), '/') ?: '', '/'); + } + + /** + * {@inheritdoc} + * + * @throws \InvalidArgumentException + */ + public function getExternalUrl() { + $dir = $this->getDirectoryPath(); + if (empty($dir)) { + throw new \InvalidArgumentException(sprintf('Extension directory for %s does not exist.', $this->uri)); + } + return $this->request->getUriForPath(base_path() . $dir . $this->getTarget()); + } + + /** + * {@inheritdoc} + */ + public function dirname($uri = NULL) { + if (!isset($uri)) { + $uri = $this->uri; + } + list($scheme) = explode('://', $uri, 2); + $target = $this->getTarget($uri); + $dirname = dirname($target); + + if ($dirname === '.' || $dirname === '\\') { + $dirname = ''; + } + + return rtrim($scheme . '://' . $this->getOwnerName() . $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..d39015f --- /dev/null +++ b/core/lib/Drupal/Core/StreamWrapper/ThemeStream.php @@ -0,0 +1,67 @@ +themeHandler = \Drupal::service('theme_handler'); + } + + /** + * {@inheritdoc} + */ + protected function getOwnerName() { + $name = parent::getOwnerName(); + + // Return name only for installed themes. + if ($this->themeHandler->themeExists($name)) { + return $name; + } + else { + // The theme does not exist or is not installed. + throw new \InvalidArgumentException(sprintf('Theme %s does not exist or is not installed', $name)); + } + } + + /** + * {@inheritdoc} + */ + protected function getDirectoryPath() { + return $this->themeHandler->getTheme($this->getOwnerName())->getPath(); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return t('Theme files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return t('Local theme files.'); + } + +} diff --git a/core/modules/color/tests/modules/color_test/color_test.module b/core/modules/color/tests/modules/color_test/color_test.module index 603906e..b89e5e4 100644 --- a/core/modules/color/tests/modules/color_test/color_test.module +++ b/core/modules/color/tests/modules/color_test/color_test.module @@ -9,6 +9,6 @@ * Implements hook_system_theme_info(). */ function color_test_system_theme_info() { - $themes['color_test_theme'] = drupal_get_path('module', 'color_test') . '/themes/color_test_theme/color_test_theme.info.yml'; + $themes['color_test_theme'] = 'module://color_test/themes/color_test_theme/color_test_theme.info.yml'; return $themes; } 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 82363fd..6780ba0 100644 --- a/core/modules/search/src/Tests/SearchSimplifyTest.php +++ b/core/modules/search/src/Tests/SearchSimplifyTest.php @@ -25,7 +25,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/src/Tests/File/SystemStreamTest.php b/core/modules/system/src/Tests/File/SystemStreamTest.php new file mode 100644 index 0000000..4bb8b0e --- /dev/null +++ b/core/modules/system/src/Tests/File/SystemStreamTest.php @@ -0,0 +1,483 @@ +streamWrapperManager = $this->container->get('stream_wrapper_manager'); + + /** @var \Drupal\Core\State\StateInterface $state */ + $state = $this->container->get('state'); + // Add 'minimal' profile to 'system.module.files' state to allow + // drupal_get_profile() to get the profile from state. drupal_get_profile() + // resolves only the current profile but reads also profiles from states. + // @see drupal_get_profile() + $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']); + } + + /** + * Test Invalid stream uri. + */ + public function testInvalidStreamUriException() { + $bad_uris = [ + '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', + ]; + + $instance = $this->streamWrapperManager->getViaScheme('module'); + + foreach ($bad_uris as $bad_uri) { + try { + $instance->dirname($bad_uri); + $this->fail(SafeMarkup::format('Invalid uri %uri not detected.', ['%uri' => $bad_uri])); + } + catch (\InvalidArgumentException $e) { + if ($e->getMessage() == 'Malformed uri parameter passed: ' . $bad_uri) { + $this->pass(SafeMarkup::format('Throw exception on invalid uri %uri supplied.', ['%uri' => $bad_uri])); + } + else { + throw new \InvalidArgumentException($e); + } + } + } + } + + /** + * Test the Module stream wrapper functions. + */ + public function testModuleStream() { + // Generate a module stream wrapper instance. + $instance = $this->streamWrapperManager->getViaScheme('module'); + + // Test dirname(). + $data = $this->moduleStreamTestCasesProvider('dirname'); + $this->runTestOnInstanceMethod($instance, 'dirname', $data); + + // Test realpath(). + $data = $this->moduleStreamTestCasesProvider('realpath'); + $this->runTestOnInstanceMethod($instance, 'realpath', $data); + + // Test getExternalUrl(). + $data = $this->moduleStreamTestCasesProvider('getExternalUrl'); + $this->runTestOnInstanceMethod($instance, 'getExternalUrl', $data); + } + + /** + * Provides test cases for testModuleStream(). + * + * @param string $method + * The method to be tested. + * + * @return array + * Associative array with test cases as values, keyed by uri. + */ + protected function moduleStreamTestCasesProvider($method) { + $uris = [ + 'module://system', + 'module://system/css/system.admin.css', + 'module://file_test/file_test.dummy.inc', + 'module://file_test/src/file_test.dummy.inc', + 'module://ckeditor/ckeditor.info.yml', + 'module://foo_bar/foo.bar.js', + ]; + switch ($method) { + case 'dirname': + return array_combine($uris, [ + 'module://system', + 'module://system/css', + 'module://file_test', + 'module://file_test/src', + new \InvalidArgumentException('Module ckeditor does not exist or is not installed'), + new \InvalidArgumentException('Module foo_bar does not exist or is not installed'), + ]); + case 'realpath': + return array_combine($uris, [ + DRUPAL_ROOT . '/core/modules/system', + DRUPAL_ROOT . '/core/modules/system/css/system.admin.css', + DRUPAL_ROOT . '/core/modules/file/tests/file_test/file_test.dummy.inc', + DRUPAL_ROOT . '/core/modules/file/tests/file_test/src/file_test.dummy.inc', + new \InvalidArgumentException('Module ckeditor does not exist or is not installed'), + new \InvalidArgumentException('Module foo_bar does not exist or is not installed'), + ]); + case 'getExternalUrl': + $base_url = \Drupal::request()->getUriForPath(base_path()); + return array_combine($uris, [ + $base_url . 'core/modules/system', + $base_url . 'core/modules/system/css/system.admin.css', + $base_url . 'core/modules/file/tests/file_test/file_test.dummy.inc', + $base_url . 'core/modules/file/tests/file_test/src/file_test.dummy.inc', + new \InvalidArgumentException('Module ckeditor does not exist or is not installed'), + new \InvalidArgumentException('Module foo_bar does not exist or is not installed'), + ]); + } + } + + /** + * Test the Profile stream wrapper functions. + */ + public function testProfileStream() { + // Generate a profile stream wrapper instance. + $instance = $this->streamWrapperManager->getViaScheme('profile'); + + // Test dirname(). + $data = $this->profileStreamTestCasesProvider('dirname'); + $this->runTestOnInstanceMethod($instance, 'dirname', $data); + + // Test realpath(). + $data = $this->profileStreamTestCasesProvider('realpath'); + $this->runTestOnInstanceMethod($instance, 'realpath', $data); + + // Test getExternalUrl(). + $data = $this->profileStreamTestCasesProvider('getExternalUrl'); + $this->runTestOnInstanceMethod($instance, 'getExternalUrl', $data); + } + + /** + * Provides test cases for testProfileStream(). + * + * @param string $method + * The method to be tested. + * + * @return array + * Associative array with test cases as values, keyed by uri. + */ + protected function profileStreamTestCasesProvider($method) { + $uris = [ + 'profile://minimal', + 'profile://minimal/config/install/block.block.stark_login.yml', + 'profile://minimal/config/install/node.type.article.yml', + 'profile://foo_bar/', + ]; + + switch ($method) { + case 'dirname': + return array_combine($uris, [ + 'profile://minimal', + 'profile://minimal/config/install', + 'profile://minimal/config/install', + new \InvalidArgumentException('Profile foo_bar does not exist'), + ]); + case 'realpath': + return array_combine($uris, [ + DRUPAL_ROOT . '/core/profiles/minimal', + DRUPAL_ROOT . '/core/profiles/minimal/config/install/block.block.stark_login.yml', + DRUPAL_ROOT . '/core/profiles/minimal/config/install/node.type.article.yml', + new \InvalidArgumentException('Profile foo_bar does not exist'), + ]); + case 'getExternalUrl': + $base_url = \Drupal::request()->getUriForPath(base_path()); + return array_combine($uris, [ + $base_url . 'core/profiles/minimal', + $base_url . 'core/profiles/minimal/config/install/block.block.stark_login.yml', + $base_url . 'core/profiles/minimal/config/install/node.type.article.yml', + new \InvalidArgumentException('Profile foo_bar does not exist'), + ]); + } + } + + /** + * Test the installed profile stream wrapper functions. + */ + public function testInstalledProfileStream() { + // Generate a profile stream wrapper instance. + $instance = $this->streamWrapperManager->getViaScheme('installed-profile'); + + // Test dirname(). + $data = $this->installedProfileStreamTestCasesProvider('dirname'); + $this->runTestOnInstanceMethod($instance, 'dirname', $data); + + // Test realpath(). + $data = $this->installedProfileStreamTestCasesProvider('realpath'); + $this->runTestOnInstanceMethod($instance, 'realpath', $data); + + // Test getExternalUrl(). + $data = $this->installedProfileStreamTestCasesProvider('getExternalUrl'); + $this->runTestOnInstanceMethod($instance, 'getExternalUrl', $data); + } + + /** + * Provides test cases for testProfileStream(). + * + * @param string $method + * The method to be tested. + * + * @return array + * Associative array with test cases as values, keyed by uri. + */ + protected function installedProfileStreamTestCasesProvider($method) { + $uris = [ + 'installed-profile://', + 'installed-profile://config/install/block.block.stark_login.yml', + 'installed-profile://config/install/node.type.article.yml', + 'installed-profile://minimal.info.yml', + ]; + + switch ($method) { + case 'dirname': + return array_combine($uris, [ + 'installed-profile://', + 'installed-profile://config/install', + 'installed-profile://config/install', + 'installed-profile://', + ]); + case 'realpath': + return array_combine($uris, [ + DRUPAL_ROOT . '/core/profiles/minimal', + DRUPAL_ROOT . '/core/profiles/minimal/config/install/block.block.stark_login.yml', + DRUPAL_ROOT . '/core/profiles/minimal/config/install/node.type.article.yml', + DRUPAL_ROOT . '/core/profiles/minimal/minimal.info.yml', + ]); + case 'getExternalUrl': + $base_url = \Drupal::request()->getUriForPath(base_path()); + return array_combine($uris, [ + $base_url . 'core/profiles/minimal', + $base_url . 'core/profiles/minimal/config/install/block.block.stark_login.yml', + $base_url . 'core/profiles/minimal/config/install/node.type.article.yml', + $base_url . 'core/profiles/minimal/minimal.info.yml', + ]); + } + } + + /** + * Test the Theme stream wrapper functions. + */ + public function testThemeStream() { + /** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */ + $theme_installer = $this->container->get('theme_installer'); + // Install Bartik and Seven themes. + $theme_installer->install(['bartik', 'seven']); + + // Generate a theme stream wrapper instance. + $instance = $this->streamWrapperManager->getViaScheme('theme'); + + // Test dirname(). + $data = $this->themeStreamTestCasesProvider('dirname'); + $this->runTestOnInstanceMethod($instance, 'dirname', $data); + + // Test realpath(). + $data = $this->themeStreamTestCasesProvider('realpath'); + $this->runTestOnInstanceMethod($instance, 'realpath', $data); + + // Test getExternalUrl(). + $data = $this->themeStreamTestCasesProvider('getExternalUrl'); + $this->runTestOnInstanceMethod($instance, 'getExternalUrl', $data); + } + + /** + * Provides test cases for testThemeStream(). + * + * @param string $method + * The method to be tested. + * + * @return array + * Associative array with test cases as values, keyed by uri. + */ + protected function themeStreamTestCasesProvider($method) { + $uris = [ + 'theme://seven', + 'theme://seven/style.css', + 'theme://bartik/color/preview.js', + 'theme://fifteen/screenshot.png', + 'theme://stark/stark.info.yml', + ]; + + switch ($method) { + case 'dirname': + return array_combine($uris, [ + 'theme://seven', + 'theme://seven', + 'theme://bartik/color', + new \InvalidArgumentException('Theme fifteen does not exist or is not installed'), + new \InvalidArgumentException('Theme stark does not exist or is not installed'), + ]); + case 'realpath': + return array_combine($uris, [ + DRUPAL_ROOT . '/core/themes/seven', + DRUPAL_ROOT . '/core/themes/seven/style.css', + DRUPAL_ROOT . '/core/themes/bartik/color/preview.js', + new \InvalidArgumentException('Theme fifteen does not exist or is not installed'), + new \InvalidArgumentException('Theme stark does not exist or is not installed'), + ]); + case 'getExternalUrl': + $base_url = \Drupal::request()->getUriForPath(base_path()); + return array_combine($uris, [ + $base_url . 'core/themes/seven', + $base_url . 'core/themes/seven/style.css', + $base_url . 'core/themes/bartik/color/preview.js', + new \InvalidArgumentException('Theme fifteen does not exist or is not installed'), + new \InvalidArgumentException('Theme stark does not exist or is not installed'), + ]); + } + } + + /** + * Test the pseudo theme stream wrapper functions. + */ + public function testPseudoThemeStreams() { + /** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */ + $config_factory = $this->container->get('config.factory'); + /** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */ + $theme_installer = $this->container->get('theme_installer'); + + // Install Bartik theme. + $theme_installer->install(['bartik']); + // Set default and admin theme to Bartik. + $config_factory->getEditable('system.theme') + ->set('default', 'bartik') + ->set('admin', 'bartik') + ->save(); + + foreach (['active', 'default', 'admin'] as $scheme) { + $scheme .= '-theme'; + + // Generate a theme stream wrapper instance. + $instance = $this->streamWrapperManager->getViaScheme($scheme); + + // Test dirname(). + $data = $this->pseudoThemeStreamTestCasesProvider('dirname', $scheme); + $this->runTestOnInstanceMethod($instance, 'dirname', $data); + + // Test realpath(). + $data = $this->pseudoThemeStreamTestCasesProvider('realpath', $scheme); + $this->runTestOnInstanceMethod($instance, 'realpath', $data); + + // Test getExternalUrl(). + $data = $this->pseudoThemeStreamTestCasesProvider('getExternalUrl', $scheme); + $this->runTestOnInstanceMethod($instance, 'getExternalUrl', $data); + } + } + + /** + * Provides test cases for testPseudoThemeStreams(). + * + * @param string $method + * The method to be tested. + * @param string $scheme + * The scheme to be tested. + * + * @return array + * Associative array with test cases as values, keyed by uri. + */ + protected function pseudoThemeStreamTestCasesProvider($method, $scheme) { + $uris = [ + "$scheme://", + "$scheme://logo.png", + "$scheme://color/preview.js", + ]; + + switch ($method) { + case 'dirname': + return array_combine($uris, [ + "$scheme://", + "$scheme://", + "$scheme://color", + ]); + case 'realpath': + return array_combine($uris, [ + DRUPAL_ROOT . '/core/themes/bartik', + DRUPAL_ROOT . '/core/themes/bartik/logo.png', + DRUPAL_ROOT . '/core/themes/bartik/color/preview.js', + ]); + case 'getExternalUrl': + $base_url = \Drupal::request()->getUriForPath(base_path()); + return array_combine($uris, [ + $base_url . 'core/themes/bartik', + $base_url . 'core/themes/bartik/logo.png', + $base_url . 'core/themes/bartik/color/preview.js', + ]); + } + } + + /** + * Helper method to run specific tests on each StreamWrapper method. + * + * @param \Drupal\Core\StreamWrapper\StreamWrapperInterface $instance + * The stream wrapper instance to carry out the test on. + * @param string $method + * The SystemStream method to be tested. + * @param array $data + * An array of data to be used for the test, containing the following info: + * - The expected result. + * - The test result message. + */ + private function runTestOnInstanceMethod(StreamWrapperInterface $instance, $method, array $data) { + foreach ($data as $uri => $case) { + $instance->setUri($uri); + if ($case instanceof \InvalidArgumentException) { + /** @var \InvalidArgumentException $case */ + $message = sprintf('Exception thrown: \InvalidArgumentException("%s").', $case->getMessage()); + try { + $instance->$method(); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->assertIdentical($e->getMessage(), $case->getMessage(), $message); + } + } + elseif (is_string($case)) { + $this->assertEqual($instance->$method(), $case); + } + } + } + + /** + * {@inheritdoc} + */ + public function errorHandler($severity, $message, $file = NULL, $line = NULL) { + // When dealing with an unknown module/profile, drupal_get_filename() + // triggers an error. We want to mute this error because we catch this as + // exception. + // @see drupal_get_filename() + if ($severity != E_USER_WARNING || strpos($message, 'The following module is missing from the file system: ') !== 0) { + parent::errorHandler($severity, $message, $file, $line); + } + } + +}