diff --git a/core/includes/common.inc b/core/includes/common.inc index bffacfa..63c59c9 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -485,7 +485,7 @@ function drupal_get_query_array($query) { if (!empty($query)) { foreach (explode('&', $query) as $param) { $param = explode('=', $param); - $result[$param[0]] = isset($param[1]) ? rawurldecode($param[1]) : ''; + $result[$param[0]] = isset($param[1]) ? rawurldecode($param[1]) : NULL; } } return $result; diff --git a/core/includes/file.inc b/core/includes/file.inc index 58644ff..c26615a 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -7,9 +7,6 @@ use Drupal\Core\StreamWrapper\LocalStream; use Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\HttpFoundation\BinaryFileResponse; /** * Stream wrapper bit flags that are the basis for composite types. @@ -1332,42 +1329,6 @@ function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EX } /** - * Page callback: Handles private file transfers. - * - * Call modules that implement hook_file_download() to find out if a file is - * accessible and what headers it should be transferred with. If one or more - * modules returned headers the download will start with the returned headers. - * If a module returns -1 an AccessDeniedHttpException will be thrown. - * If the file exists but no modules responded an AccessDeniedHttpException will - * be thrown.If the file does not exist a NotFoundHttpException will be thrown. - * - * @see hook_file_download() - * @see system_menu() - */ -function file_download() { - // Merge remaining path arguments into relative file path. - $args = func_get_args(); - $scheme = array_shift($args); - $target = implode('/', $args); - $uri = $scheme . '://' . $target; - if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) { - // Let other modules provide headers and controls access to the file. - $headers = module_invoke_all('file_download', $uri); - foreach ($headers as $result) { - if ($result == -1) { - throw new AccessDeniedHttpException(); - } - } - if (count($headers)) { - return new BinaryFileResponse($uri, 200, $headers); - } - throw new AccessDeniedHttpException(); - } - throw new NotFoundHttpException(); -} - - -/** * Finds all files that match a given mask in a given directory. * * Directories and files beginning with a period are excluded; this diff --git a/core/modules/config/lib/Drupal/config/Controller/ConfigController.php b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php index 5709feb..3f54d0a 100644 --- a/core/modules/config/lib/Drupal/config/Controller/ConfigController.php +++ b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php @@ -12,7 +12,10 @@ use Drupal\Component\Archiver\ArchiveTar; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\OpenModalDialogCommand; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\system\FileController; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; /** * Returns responses for config module routes. @@ -34,10 +37,21 @@ class ConfigController implements ControllerInterface { protected $sourceStorage; /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static($container->get('config.storage'), $container->get('config.storage.staging')); + return new static( + $container->get('config.storage'), + $container->get('config.storage.staging'), + $container->get('module_handler') + ); } /** @@ -47,10 +61,13 @@ public static function create(ContainerInterface $container) { * The target storage. * @param \Drupal\Core\Config\StorageInterface $source_storage * The source storage + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. */ - public function __construct(StorageInterface $target_storage, StorageInterface $source_storage) { + public function __construct(StorageInterface $target_storage, StorageInterface $source_storage, ModuleHandlerInterface $module_handler) { $this->targetStorage = $target_storage; $this->sourceStorage = $source_storage; + $this->moduleHandler = $module_handler; } /** @@ -64,7 +81,10 @@ public function downloadExport() { $config_files[] = $config_dir . '/' . $config_name . '.yml'; } $archiver->createModify($config_files, '', config_get_config_directory()); - return file_download('temporary', 'config.tar.gz'); + + $file_controller = new FileController($this->moduleHandler); + $request = new Request(array('file' => 'config.tar.gz')); + return $file_controller->fileDownload($request, 'temporary'); } /** diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 4a45b51..4a41482 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -1579,7 +1579,7 @@ function file_get_file_references(File $file, $field = NULL, $age = FIELD_LOAD_R // for every revision or the entity does not support revisions then // every usage is already a match. $match_entity_type = $age == FIELD_LOAD_REVISION || !isset($entity_info['entity_keys']['revision']); - $entities = entity_load_multiple($entity_type, $entity_ids); + $entities = entity_load_multiple($entity_type, array_keys($entity_ids)); foreach ($entities as $entity) { $bundle = $entity->bundle(); // We need to find file fields for this entity type and bundle. diff --git a/core/modules/image/image.module b/core/modules/image/image.module index 53cb813..beb4f05 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -97,28 +97,6 @@ function image_style_entity_uri(ImageStyle $style) { function image_menu() { $items = array(); - // Generate image derivatives of publicly available files. - // If clean URLs are disabled, image derivatives will always be served - // through the menu system. - // If clean URLs are enabled and the image derivative already exists, - // PHP will be bypassed. - $directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath(); - $items[$directory_path . '/styles/%image_style'] = array( - 'title' => 'Generate image style', - 'page callback' => 'image_style_deliver', - 'page arguments' => array(count(explode('/', $directory_path)) + 1), - 'access callback' => TRUE, - 'type' => MENU_CALLBACK, - ); - // Generate and deliver image derivatives of private files. - // These image derivatives are always delivered through the menu system. - $items['system/files/styles/%image_style'] = array( - 'title' => 'Generate image style', - 'page callback' => 'image_style_deliver', - 'page arguments' => array(3), - 'access callback' => TRUE, - 'type' => MENU_CALLBACK, - ); $items['admin/config/media/image-styles'] = array( 'title' => 'Image styles', 'description' => 'Configure styles that can be used for resizing or adjusting images on display.', @@ -516,98 +494,6 @@ function image_style_options($include_empty = TRUE) { } /** - * Page callback: Generates a derivative, given a style and image path. - * - * After generating an image, transfer it to the requesting agent. - * - * @param $style - * The image style - */ -function image_style_deliver($style, $scheme) { - $args = func_get_args(); - array_shift($args); - array_shift($args); - $target = implode('/', $args); - - // Check that the style is defined, the scheme is valid, and the image - // derivative token is valid. (Sites which require image derivatives to be - // generated without a token can set the - // 'image.settings:allow_insecure_derivatives' configuration to TRUE to bypass - // the latter check, but this will increase the site's vulnerability to - // denial-of-service attacks.) - $valid = !empty($style) && file_stream_wrapper_valid_scheme($scheme); - if (!config('image.settings')->get('allow_insecure_derivatives')) { - $image_derivative_token = Drupal::request()->query->get(IMAGE_DERIVATIVE_TOKEN); - $valid = $valid && isset($image_derivative_token) && $image_derivative_token === image_style_path_token($style->name, $scheme . '://' . $target); - } - if (!$valid) { - throw new AccessDeniedHttpException(); - } - - $image_uri = $scheme . '://' . $target; - $derivative_uri = image_style_path($style->id(), $image_uri); - - // If using the private scheme, let other modules provide headers and - // control access to the file. - if ($scheme == 'private') { - if (file_exists($derivative_uri)) { - file_download($scheme, file_uri_target($derivative_uri)); - } - else { - $headers = module_invoke_all('file_download', $image_uri); - if (in_array(-1, $headers) || empty($headers)) { - throw new AccessDeniedHttpException(); - } - if (count($headers)) { - foreach ($headers as $name => $value) { - drupal_add_http_header($name, $value); - } - } - } - } - - // Don't try to generate file if source is missing. - if (!file_exists($image_uri)) { - watchdog('image', 'Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', array('%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri)); - return new Response(t('Error generating image, missing source file.'), 404); - } - - // Don't start generating the image if the derivative already exists or if - // generation is in progress in another thread. - $lock_name = 'image_style_deliver:' . $style->id() . ':' . Crypt::hashBase64($image_uri); - if (!file_exists($derivative_uri)) { - $lock_acquired = lock()->acquire($lock_name); - if (!$lock_acquired) { - // Tell client to retry again in 3 seconds. Currently no browsers are known - // to support Retry-After. - throw new ServiceUnavailableHttpException(3, t('Image generation in progress. Try again shortly.')); - } - } - - // Try to generate the image, unless another thread just did it while we were - // acquiring the lock. - $success = file_exists($derivative_uri) || image_style_create_derivative($style, $image_uri, $derivative_uri); - - if (!empty($lock_acquired)) { - lock()->release($lock_name); - } - - if ($success) { - $image = image_load($derivative_uri); - $uri = $image->source; - $headers = array( - 'Content-Type' => $image->info['mime_type'], - 'Content-Length' => $image->info['file_size'], - ); - return new BinaryFileResponse($uri, 200, $headers); - } - else { - watchdog('image', 'Unable to generate the derived image located at %path.', array('%path' => $derivative_uri)); - return new Response(t('Error generating image.'), 500); - } -} - -/** * Creates a new image derivative based on an image style. * * Generates an image derivative by creating the destination folder (if it does @@ -765,9 +651,10 @@ function image_style_url($style_name, $path, $clean_urls = NULL) { } $file_url = file_create_url($uri); + // Append the query string with the token, if necessary. if ($token_query) { - $file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query); + $file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . Drupal::urlGenerator()->httpBuildQuery($token_query); } return $file_url; diff --git a/core/modules/image/image.routing.yml b/core/modules/image/image.routing.yml index b178d33..3d6c8c3 100644 --- a/core/modules/image/image.routing.yml +++ b/core/modules/image/image.routing.yml @@ -11,3 +11,11 @@ image_effect_delete: _form: '\Drupal\image\Form\ImageEffectDeleteForm' requirements: _permission: 'administer image styles' + +image_style_private: + pattern: '/system/files/styles/{image_style}/{scheme}' + defaults: + _controller: '\Drupal\image\Controller\ImageStyleController::deliver' + requirements: + _access: 'TRUE' + diff --git a/core/modules/image/image.services.yml b/core/modules/image/image.services.yml new file mode 100644 index 0000000..2661593 --- /dev/null +++ b/core/modules/image/image.services.yml @@ -0,0 +1,9 @@ +services: + image.route_subscriber: + class: Drupal\image\EventSubscriber\RouteSubscriber + tags: + - { name: 'event_subscriber' } + path_processor.image_styles: + class: Drupal\image\PathProcessor\PathProcessorImageStyles + tags: + - { name: path_processor_inbound, priority: 300 } diff --git a/core/modules/image/lib/Drupal/image/Controller/ImageStyleController.php b/core/modules/image/lib/Drupal/image/Controller/ImageStyleController.php new file mode 100644 index 0000000..ea88fd3 --- /dev/null +++ b/core/modules/image/lib/Drupal/image/Controller/ImageStyleController.php @@ -0,0 +1,181 @@ +configFactory = $config_factory; + $this->moduleHandler = $module_handler; + $this->lock = $lock; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('module_handler'), + $container->get('lock') + ); + } + + /** + * Generates a derivative, given a style and image path. + * + * After generating an image, transfer it to the requesting agent. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * @param string $scheme + * The file scheme, defaults to 'private'. + * @param \Drupal\image\ImageStyleInterface $image_style + * The image style to deliver. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * Thrown when the user does not have access to the file. + * + * @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response + * The transferred file as response or some error response. + */ + public function deliver(Request $request, $scheme, ImageStyleInterface $image_style) { + $target = $request->query->get('file'); + + // Check that the style is defined, the scheme is valid, and the image + // derivative token is valid. (Sites which require image derivatives to be + // generated without a token can set the + // 'image.settings:allow_insecure_derivatives' configuration to TRUE to bypass + // the latter check, but this will increase the site's vulnerability to + // denial-of-service attacks.) + $valid = !empty($image_style) && file_stream_wrapper_valid_scheme($scheme); + if (!$this->configFactory->get('image.settings')->get('allow_insecure_derivatives')) { + $valid = $valid && $request->query->get(IMAGE_DERIVATIVE_TOKEN) === image_style_path_token($image_style->name, $scheme . '://' . $target); + } + if (!$valid) { + throw new AccessDeniedHttpException(); + } + + $image_uri = $scheme . '://' . $target; + $derivative_uri = image_style_path($image_style->id(), $image_uri); + + $response = new Response(''); + + // If using the private scheme, let other modules provide headers and + // control access to the file. + if ($scheme == 'private') { + if (file_exists($derivative_uri)) { + return $this->fileDownload($request, $scheme); + } + else { + $headers = $this->moduleHandler->invokeAll('file_download', array($image_uri)); + if (in_array(-1, $headers) || empty($headers)) { + throw new AccessDeniedHttpException(); + } + if (count($headers)) { + foreach ($headers as $name => $value) { + $response->headers->set($name, $value); + } + } + } + } + + // Don't try to generate file if source is missing. + if (!file_exists($image_uri)) { + watchdog('image', 'Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', array('%source_image_path' => $image_uri, '%derivative_path' => $derivative_uri)); + $response->setContent(t('Error generating image, missing source file.')); + $response->setStatusCode(404); + return $response; + } + + // Don't start generating the image if the derivative already exists or if + // generation is in progress in another thread. + $lock_name = 'image_style_deliver:' . $image_style->id() . ':' . Crypt::hashBase64($image_uri); + if (!file_exists($derivative_uri)) { + $lock_acquired = $this->lock->acquire($lock_name); + if (!$lock_acquired) { + // Tell client to retry again in 3 seconds. Currently no browsers are known + // to support Retry-After. + $response->headers->set('Status', '503 Service Unavailable'); + $response->headers->set('Retry-After', 3); + $response->setContent(t('Image generation in progress. Try again shortly.')); + return $response; + } + } + + // Try to generate the image, unless another thread just did it while we were + // acquiring the lock. + $success = file_exists($derivative_uri) || image_style_create_derivative($image_style, $image_uri, $derivative_uri); + + if (!empty($lock_acquired)) { + $this->lock->release($lock_name); + } + + if ($success) { + $image = image_load($derivative_uri); + $uri = $image->source; + $response->headers->set('Content-Type', $image->info['mime_type']); + $response->headers->set('Content-Length', $image->info['file_size']); + return new BinaryFileResponse($uri, 200, $response->headers->all()); + } + else { + watchdog('image', 'Unable to generate the derived image located at %path.', array('%path' => $derivative_uri)); + $response->setContent(t('Error generating image.')); + $response->setStatusCode(500); + return $response; + } + } + +} diff --git a/core/modules/image/lib/Drupal/image/EventSubscriber/RouteSubscriber.php b/core/modules/image/lib/Drupal/image/EventSubscriber/RouteSubscriber.php new file mode 100644 index 0000000..c0dde68 --- /dev/null +++ b/core/modules/image/lib/Drupal/image/EventSubscriber/RouteSubscriber.php @@ -0,0 +1,55 @@ +getRouteCollection(); + + $directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath(); + + $route = new Route('/' . $directory_path . '/styles/{image_style}/{scheme}', + array( + '_controller' => 'Drupal\image\Controller\ImageStyleController::deliver', + ), + array( + '_access' => 'TRUE', + ) + ); + $collection->add('image_style_public', $route); + } + +} diff --git a/core/modules/image/lib/Drupal/image/PathProcessor/PathProcessorImageStyles.php b/core/modules/image/lib/Drupal/image/PathProcessor/PathProcessorImageStyles.php new file mode 100644 index 0000000..ea26f2f --- /dev/null +++ b/core/modules/image/lib/Drupal/image/PathProcessor/PathProcessorImageStyles.php @@ -0,0 +1,77 @@ +getDirectoryPath(); + if (strpos($path, $directory_path . '/styles/') === 0) { + $rest = str_replace($directory_path .'/styles/', '', $path); + + // Get the image style, schema and actual url. + list($image_style, $scheme, $rest) = explode('/', $rest, 3); + + // Set file as query parameter on the route. There might be additional + // keys like itok, set them as query parameter as well. + $query = drupal_get_query_array($rest); + foreach ($query as $key => $value) { + if (!isset($value) && !$request->query->has('file')) { + $request->query->set('file', $key); + } + else { + $request->query->set($key, $value); + } + } + $path = $directory_path . '/styles/' . $image_style .'/' . $scheme; + } + elseif (strpos($path, 'system/files/styles/') === 0) { + $rest = str_replace('system/files/styles/', '', $path); + + // Get the image style, schema and actual url. + list($image_style, $scheme, $file_path) = explode('/', $rest); + + // Set file as query parameter on the route. There might be additional + // keys like itok, set them as query parameter as well. + $query = drupal_get_query_array($file_path); + foreach ($query as $key => $value) { + if (!isset($value) && !$request->query->has('file')) { + $request->query->set('file', $key); + } + else { + $request->query->set($key, $value); + } + } + $path = 'system/files/styles/' . $image_style . '/' . $scheme; + } + return $path; + } + +} diff --git a/core/modules/system/lib/Drupal/system/FileController.php b/core/modules/system/lib/Drupal/system/FileController.php new file mode 100644 index 0000000..d60e459 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/FileController.php @@ -0,0 +1,100 @@ +moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('module_handler') + ); + } + + /** + * Handles private file transfers. + * + * Call modules that implement hook_file_download() to find out if a file is + * accessible and what headers it should be transferred with. If one or more + * modules returned headers the download will start with the returned headers. + * If a module returns -1 an AccessDeniedHttpException will be thrown. If the + * file exists but no modules responded an AccessDeniedHttpException will be + * thrown. If the file does not exist a NotFoundHttpException will be thrown. + * + * @see hook_file_download() + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * @param int|string $scheme + * The file scheme, defaults to 'private'. + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * Thrown when the requested file does not exist. + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * Thrown when the user does not have access to the file. + * + * @return \Symfony\Component\HttpFoundation\BinaryFileResponse + * The transferred file as response. + */ + public function fileDownload(Request $request, $scheme = 'private') { + $target = $request->query->get('file'); + // Merge remaining path arguments into relative file path. + $uri = $scheme . '://' . $target; + + if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) { + // Let other modules provide headers and controls access to the file. + $headers = $this->moduleHandler->invokeAll('file_download', array($uri)); + + foreach ($headers as $result) { + if ($result == -1) { + throw new AccessDeniedHttpException(); + } + } + + if (count($headers)) { + return new BinaryFileResponse($uri, 200, $headers); + } + + throw new AccessDeniedHttpException(); + } + + throw new NotFoundHttpException(); + } + +} diff --git a/core/modules/system/lib/Drupal/system/PathProcessor/PathProcessorFiles.php b/core/modules/system/lib/Drupal/system/PathProcessor/PathProcessorFiles.php new file mode 100644 index 0000000..d9cf07a --- /dev/null +++ b/core/modules/system/lib/Drupal/system/PathProcessor/PathProcessorFiles.php @@ -0,0 +1,33 @@ +query->has('file')) { + $file_path = str_replace('system/files/', '', $path); + $request->query->set('file', $file_path); + return 'system/files'; + } + return $path; + } + +} diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 15aa52c..272b854 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -614,13 +614,6 @@ function system_element_info() { * Implements hook_menu(). */ function system_menu() { - $items['system/files'] = array( - 'title' => 'File download', - 'page callback' => 'file_download', - 'page arguments' => array('private'), - 'access callback' => TRUE, - 'type' => MENU_CALLBACK, - ); $items['system/temporary'] = array( 'title' => 'Temporary files', 'page callback' => 'file_download', diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 45ce1e5..9644786 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -144,6 +144,14 @@ system_admin_index: requirements: _permission: 'access administration pages' +system_files: + pattern: '/system/files/{scheme}' + defaults: + _controller: 'Drupal\system\FileController::fileDownload' + scheme: private + requirements: + _access: 'TRUE' + system_theme_settings: pattern: '/admin/appearance/settings' defaults: diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index 6aefa00..d9f29f7 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -13,6 +13,10 @@ services: class: Drupal\system\LegacyBreadcrumbBuilder tags: - {name: breadcrumb_builder, priority: 500} + path_processor.files: + class: Drupal\system\PathProcessor\PathProcessorFiles + tags: + - { name: path_processor_inbound, priority: 200 } system.route_subscriber: class: Drupal\system\Routing\RouteSubscriber tags: