"Fossies" - the Fresh Open Source Software Archive

Member "drupal-8.9.10/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php" (26 Nov 2020, 22345 Bytes) of package /linux/www/drupal-8.9.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "FileUploadResource.php" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 9.0.8_vs_9.1.0-rc1.

    1 <?php
    2 
    3 namespace Drupal\file\Plugin\rest\resource;
    4 
    5 use Drupal\Component\Utility\Bytes;
    6 use Drupal\Component\Utility\Crypt;
    7 use Drupal\Component\Utility\Environment;
    8 use Drupal\Core\Config\Config;
    9 use Drupal\Core\Entity\EntityTypeManagerInterface;
   10 use Drupal\Core\Field\FieldDefinitionInterface;
   11 use Drupal\Core\File\Exception\FileException;
   12 use Drupal\Core\File\FileSystemInterface;
   13 use Drupal\Core\Lock\LockBackendInterface;
   14 use Drupal\Core\Session\AccountInterface;
   15 use Drupal\Core\Utility\Token;
   16 use Drupal\file\FileInterface;
   17 use Drupal\rest\ModifiedResourceResponse;
   18 use Drupal\rest\Plugin\ResourceBase;
   19 use Drupal\Component\Render\PlainTextOutput;
   20 use Drupal\Core\Entity\EntityFieldManagerInterface;
   21 use Drupal\file\Entity\File;
   22 use Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait;
   23 use Drupal\rest\RequestHandler;
   24 use Psr\Log\LoggerInterface;
   25 use Symfony\Component\DependencyInjection\ContainerInterface;
   26 use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
   27 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
   28 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
   29 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
   30 use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
   31 use Symfony\Component\Routing\Route;
   32 use Symfony\Component\HttpFoundation\Request;
   33 use Symfony\Component\HttpKernel\Exception\HttpException;
   34 
   35 /**
   36  * File upload resource.
   37  *
   38  * This is implemented as a field-level resource for the following reasons:
   39  *   - Validation for uploaded files is tied to fields (allowed extensions, max
   40  *     size, etc..).
   41  *   - The actual files do not need to be stored in another temporary location,
   42  *     to be later moved when they are referenced from a file field.
   43  *   - Permission to upload a file can be determined by a users field level
   44  *     create access to the file field.
   45  *
   46  * @RestResource(
   47  *   id = "file:upload",
   48  *   label = @Translation("File Upload"),
   49  *   serialization_class = "Drupal\file\Entity\File",
   50  *   uri_paths = {
   51  *     "https://www.drupal.org/link-relations/create" = "/file/upload/{entity_type_id}/{bundle}/{field_name}"
   52  *   }
   53  * )
   54  */
   55 class FileUploadResource extends ResourceBase {
   56 
   57   use EntityResourceValidationTrait {
   58     validate as resourceValidate;
   59   }
   60 
   61   /**
   62    * The regex used to extract the filename from the content disposition header.
   63    *
   64    * @var string
   65    */
   66   const REQUEST_HEADER_FILENAME_REGEX = '@\bfilename(?<star>\*?)=\"(?<filename>.+)\"@';
   67 
   68   /**
   69    * The amount of bytes to read in each iteration when streaming file data.
   70    *
   71    * @var int
   72    */
   73   const BYTES_TO_READ = 8192;
   74 
   75   /**
   76    * The file system service.
   77    *
   78    * @var \Drupal\Core\File\FileSystem
   79    */
   80   protected $fileSystem;
   81 
   82   /**
   83    * The entity type manager.
   84    *
   85    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   86    */
   87   protected $entityTypeManager;
   88 
   89   /**
   90    * The entity field manager.
   91    *
   92    * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   93    */
   94   protected $entityFieldManager;
   95 
   96   /**
   97    * The currently authenticated user.
   98    *
   99    * @var \Drupal\Core\Session\AccountInterface
  100    */
  101   protected $currentUser;
  102 
  103   /**
  104    * The MIME type guesser.
  105    *
  106    * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
  107    */
  108   protected $mimeTypeGuesser;
  109 
  110   /**
  111    * The token replacement instance.
  112    *
  113    * @var \Drupal\Core\Utility\Token
  114    */
  115   protected $token;
  116 
  117   /**
  118    * The lock service.
  119    *
  120    * @var \Drupal\Core\Lock\LockBackendInterface
  121    */
  122   protected $lock;
  123 
  124   /**
  125    * @var \Drupal\Core\Config\ImmutableConfig
  126    */
  127   protected $systemFileConfig;
  128 
  129   /**
  130    * Constructs a FileUploadResource instance.
  131    *
  132    * @param array $configuration
  133    *   A configuration array containing information about the plugin instance.
  134    * @param string $plugin_id
  135    *   The plugin_id for the plugin instance.
  136    * @param mixed $plugin_definition
  137    *   The plugin implementation definition.
  138    * @param array $serializer_formats
  139    *   The available serialization formats.
  140    * @param \Psr\Log\LoggerInterface $logger
  141    *   A logger instance.
  142    * @param \Drupal\Core\File\FileSystemInterface $file_system
  143    *   The file system service.
  144    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
  145    *   The entity type manager.
  146    * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
  147    *   The entity field manager.
  148    * @param \Drupal\Core\Session\AccountInterface $current_user
  149    *   The currently authenticated user.
  150    * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser
  151    *   The MIME type guesser.
  152    * @param \Drupal\Core\Utility\Token $token
  153    *   The token replacement instance.
  154    * @param \Drupal\Core\Lock\LockBackendInterface $lock
  155    *   The lock service.
  156    * @param \Drupal\Core\Config\Config $system_file_config
  157    *   The system file configuration.
  158    */
  159   public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, FileSystemInterface $file_system, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AccountInterface $current_user, MimeTypeGuesserInterface $mime_type_guesser, Token $token, LockBackendInterface $lock, Config $system_file_config) {
  160     parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
  161     $this->fileSystem = $file_system;
  162     $this->entityTypeManager = $entity_type_manager;
  163     $this->entityFieldManager = $entity_field_manager;
  164     $this->currentUser = $current_user;
  165     $this->mimeTypeGuesser = $mime_type_guesser;
  166     $this->token = $token;
  167     $this->lock = $lock;
  168     $this->systemFileConfig = $system_file_config;
  169   }
  170 
  171   /**
  172    * {@inheritdoc}
  173    */
  174   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  175     return new static(
  176       $configuration,
  177       $plugin_id,
  178       $plugin_definition,
  179       $container->getParameter('serializer.formats'),
  180       $container->get('logger.factory')->get('rest'),
  181       $container->get('file_system'),
  182       $container->get('entity_type.manager'),
  183       $container->get('entity_field.manager'),
  184       $container->get('current_user'),
  185       $container->get('file.mime_type.guesser'),
  186       $container->get('token'),
  187       $container->get('lock'),
  188       $container->get('config.factory')->get('system.file')
  189     );
  190   }
  191 
  192   /**
  193    * {@inheritdoc}
  194    */
  195   public function permissions() {
  196     // Access to this resource depends on field-level access so no explicit
  197     // permissions are required.
  198     // @see \Drupal\file\Plugin\rest\resource\FileUploadResource::validateAndLoadFieldDefinition()
  199     // @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
  200     return [];
  201   }
  202 
  203   /**
  204    * Creates a file from an endpoint.
  205    *
  206    * @param \Symfony\Component\HttpFoundation\Request $request
  207    *   The current request.
  208    * @param string $entity_type_id
  209    *   The entity type ID.
  210    * @param string $bundle
  211    *   The entity bundle. This will be the same as $entity_type_id for entity
  212    *   types that don't support bundles.
  213    * @param string $field_name
  214    *   The field name.
  215    *
  216    * @return \Drupal\rest\ModifiedResourceResponse
  217    *   A 201 response, on success.
  218    *
  219    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
  220    *   Thrown when temporary files cannot be written, a lock cannot be acquired,
  221    *   or when temporary files cannot be moved to their new location.
  222    */
  223   public function post(Request $request, $entity_type_id, $bundle, $field_name) {
  224     $filename = $this->validateAndParseContentDispositionHeader($request);
  225 
  226     $field_definition = $this->validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name);
  227 
  228     $destination = $this->getUploadLocation($field_definition->getSettings());
  229 
  230     // Check the destination file path is writable.
  231     if (!$this->fileSystem->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY)) {
  232       throw new HttpException(500, 'Destination file path is not writable');
  233     }
  234 
  235     $validators = $this->getUploadValidators($field_definition);
  236 
  237     $prepared_filename = $this->prepareFilename($filename, $validators);
  238 
  239     // Create the file.
  240     $file_uri = "{$destination}/{$prepared_filename}";
  241 
  242     $temp_file_path = $this->streamUploadData();
  243 
  244     $file_uri = $this->fileSystem->getDestinationFilename($file_uri, FileSystemInterface::EXISTS_RENAME);
  245 
  246     // Lock based on the prepared file URI.
  247     $lock_id = $this->generateLockIdFromFileUri($file_uri);
  248 
  249     if (!$this->lock->acquire($lock_id)) {
  250       throw new HttpException(503, sprintf('File "%s" is already locked for writing'), NULL, ['Retry-After' => 1]);
  251     }
  252 
  253     // Begin building file entity.
  254     $file = File::create([]);
  255     $file->setOwnerId($this->currentUser->id());
  256     $file->setFilename($prepared_filename);
  257     $file->setMimeType($this->mimeTypeGuesser->guess($prepared_filename));
  258     $file->setFileUri($file_uri);
  259     // Set the size. This is done in File::preSave() but we validate the file
  260     // before it is saved.
  261     $file->setSize(@filesize($temp_file_path));
  262 
  263     // Validate the file entity against entity-level validation and field-level
  264     // validators.
  265     $this->validate($file, $validators);
  266 
  267     // Move the file to the correct location after validation. Use
  268     // FileSystemInterface::EXISTS_ERROR as the file location has already been
  269     // determined above in FileSystem::getDestinationFilename().
  270     try {
  271       $this->fileSystem->move($temp_file_path, $file_uri, FileSystemInterface::EXISTS_ERROR);
  272     }
  273     catch (FileException $e) {
  274       throw new HttpException(500, 'Temporary file could not be moved to file location');
  275     }
  276 
  277     $file->save();
  278 
  279     $this->lock->release($lock_id);
  280 
  281     // 201 Created responses return the newly created entity in the response
  282     // body. These responses are not cacheable, so we add no cacheability
  283     // metadata here.
  284     return new ModifiedResourceResponse($file, 201);
  285   }
  286 
  287   /**
  288    * Streams file upload data to temporary file and moves to file destination.
  289    *
  290    * @return string
  291    *   The temp file path.
  292    *
  293    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
  294    *   Thrown when input data cannot be read, the temporary file cannot be
  295    *   opened, or the temporary file cannot be written.
  296    */
  297   protected function streamUploadData() {
  298     // 'rb' is needed so reading works correctly on Windows environments too.
  299     $file_data = fopen('php://input', 'rb');
  300 
  301     $temp_file_path = $this->fileSystem->tempnam('temporary://', 'file');
  302     $temp_file = fopen($temp_file_path, 'wb');
  303 
  304     if ($temp_file) {
  305       while (!feof($file_data)) {
  306         $read = fread($file_data, static::BYTES_TO_READ);
  307 
  308         if ($read === FALSE) {
  309           // Close the file streams.
  310           fclose($temp_file);
  311           fclose($file_data);
  312           $this->logger->error('Input data could not be read');
  313           throw new HttpException(500, 'Input file data could not be read');
  314         }
  315 
  316         if (fwrite($temp_file, $read) === FALSE) {
  317           // Close the file streams.
  318           fclose($temp_file);
  319           fclose($file_data);
  320           $this->logger->error('Temporary file data for "%path" could not be written', ['%path' => $temp_file_path]);
  321           throw new HttpException(500, 'Temporary file data could not be written');
  322         }
  323       }
  324 
  325       // Close the temp file stream.
  326       fclose($temp_file);
  327     }
  328     else {
  329       // Close the input file stream since we can't proceed with the upload.
  330       // Don't try to close $temp_file since it's FALSE at this point.
  331       fclose($file_data);
  332       $this->logger->error('Temporary file "%path" could not be opened for file upload', ['%path' => $temp_file_path]);
  333       throw new HttpException(500, 'Temporary file could not be opened');
  334     }
  335 
  336     // Close the input stream.
  337     fclose($file_data);
  338 
  339     return $temp_file_path;
  340   }
  341 
  342   /**
  343    * Validates and extracts the filename from the Content-Disposition header.
  344    *
  345    * @param \Symfony\Component\HttpFoundation\Request $request
  346    *   The request object.
  347    *
  348    * @return string
  349    *   The filename extracted from the header.
  350    *
  351    * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
  352    *   Thrown when the 'Content-Disposition' request header is invalid.
  353    */
  354   protected function validateAndParseContentDispositionHeader(Request $request) {
  355     // Firstly, check the header exists.
  356     if (!$request->headers->has('content-disposition')) {
  357       throw new BadRequestHttpException('"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided');
  358     }
  359 
  360     $content_disposition = $request->headers->get('content-disposition');
  361 
  362     // Parse the header value. This regex does not allow an empty filename.
  363     // i.e. 'filename=""'. This also matches on a word boundary so other keys
  364     // like 'not_a_filename' don't work.
  365     if (!preg_match(static::REQUEST_HEADER_FILENAME_REGEX, $content_disposition, $matches)) {
  366       throw new BadRequestHttpException('No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided');
  367     }
  368 
  369     // Check for the "filename*" format. This is currently unsupported.
  370     if (!empty($matches['star'])) {
  371       throw new BadRequestHttpException('The extended "filename*" format is currently not supported in the "Content-Disposition" header');
  372     }
  373 
  374     // Don't validate the actual filename here, that will be done by the upload
  375     // validators in validate().
  376     // @see \Drupal\file\Plugin\rest\resource\FileUploadResource::validate()
  377     $filename = $matches['filename'];
  378 
  379     // Make sure only the filename component is returned. Path information is
  380     // stripped as per https://tools.ietf.org/html/rfc6266#section-4.3.
  381     return $this->fileSystem->basename($filename);
  382   }
  383 
  384   /**
  385    * Validates and loads a field definition instance.
  386    *
  387    * @param string $entity_type_id
  388    *   The entity type ID the field is attached to.
  389    * @param string $bundle
  390    *   The bundle the field is attached to.
  391    * @param string $field_name
  392    *   The field name.
  393    *
  394    * @return \Drupal\Core\Field\FieldDefinitionInterface
  395    *   The field definition.
  396    *
  397    * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
  398    *   Thrown when the field does not exist.
  399    * @throws \Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException
  400    *   Thrown when the target type of the field is not a file, or the current
  401    *   user does not have 'edit' access for the field.
  402    */
  403   protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name) {
  404     $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle);
  405     if (!isset($field_definitions[$field_name])) {
  406       throw new NotFoundHttpException(sprintf('Field "%s" does not exist', $field_name));
  407     }
  408 
  409     /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
  410     $field_definition = $field_definitions[$field_name];
  411     if ($field_definition->getSetting('target_type') !== 'file') {
  412       throw new AccessDeniedHttpException(sprintf('"%s" is not a file field', $field_name));
  413     }
  414 
  415     $entity_access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type_id);
  416     $bundle = $this->entityTypeManager->getDefinition($entity_type_id)->hasKey('bundle') ? $bundle : NULL;
  417     $access_result = $entity_access_control_handler->createAccess($bundle, NULL, [], TRUE)
  418       ->andIf($entity_access_control_handler->fieldAccess('edit', $field_definition, NULL, NULL, TRUE));
  419     if (!$access_result->isAllowed()) {
  420       throw new AccessDeniedHttpException($access_result->getReason());
  421     }
  422 
  423     return $field_definition;
  424   }
  425 
  426   /**
  427    * Validates the file.
  428    *
  429    * @param \Drupal\file\FileInterface $file
  430    *   The file entity to validate.
  431    * @param array $validators
  432    *   An array of upload validators to pass to file_validate().
  433    *
  434    * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
  435    *   Thrown when there are file validation errors.
  436    */
  437   protected function validate(FileInterface $file, array $validators) {
  438     $this->resourceValidate($file);
  439 
  440     // Validate the file based on the field definition configuration.
  441     $errors = file_validate($file, $validators);
  442 
  443     if (!empty($errors)) {
  444       $message = "Unprocessable Entity: file validation failed.\n";
  445       $message .= implode("\n", array_map(function ($error) {
  446         return PlainTextOutput::renderFromHtml($error);
  447       }, $errors));
  448 
  449       throw new UnprocessableEntityHttpException($message);
  450     }
  451   }
  452 
  453   /**
  454    * Prepares the filename to strip out any malicious extensions.
  455    *
  456    * @param string $filename
  457    *   The file name.
  458    * @param array $validators
  459    *   The array of upload validators.
  460    *
  461    * @return string
  462    *   The prepared/munged filename.
  463    */
  464   protected function prepareFilename($filename, array &$validators) {
  465     // Don't rename if 'allow_insecure_uploads' evaluates to TRUE.
  466     if (!$this->systemFileConfig->get('allow_insecure_uploads')) {
  467       if (!empty($validators['file_validate_extensions'][0])) {
  468         // If there is a file_validate_extensions validator and a list of
  469         // valid extensions, munge the filename to protect against possible
  470         // malicious extension hiding within an unknown file type. For example,
  471         // "filename.html.foo".
  472         $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
  473       }
  474 
  475       // Rename potentially executable files, to help prevent exploits (i.e.
  476       // will rename filename.php.foo and filename.php to filename._php._foo.txt
  477       // and filename._php.txt, respectively).
  478       if (preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename)) {
  479         // If the file will be rejected anyway due to a disallowed extension, it
  480         // should not be renamed; rather, we'll let file_validate_extensions()
  481         // reject it below.
  482         $passes_validation = FALSE;
  483         if (!empty($validators['file_validate_extensions'][0])) {
  484           $file = File::create([]);
  485           $file->setFilename($filename);
  486           $passes_validation = empty(file_validate_extensions($file, $validators['file_validate_extensions'][0]));
  487         }
  488         if (empty($validators['file_validate_extensions'][0]) || $passes_validation) {
  489           if ((substr($filename, -4) != '.txt')) {
  490             // The destination filename will also later be used to create the URI.
  491             $filename .= '.txt';
  492           }
  493           $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0] ?? '');
  494 
  495           // The .txt extension may not be in the allowed list of extensions. We
  496           // have to add it here or else the file upload will fail.
  497           if (!empty($validators['file_validate_extensions'][0])) {
  498             $validators['file_validate_extensions'][0] .= ' txt';
  499           }
  500         }
  501       }
  502     }
  503 
  504     return $filename;
  505   }
  506 
  507   /**
  508    * Determines the URI for a file field.
  509    *
  510    * @param array $settings
  511    *   The array of field settings.
  512    *
  513    * @return string
  514    *   An un-sanitized file directory URI with tokens replaced. The result of
  515    *   the token replacement is then converted to plain text and returned.
  516    */
  517   protected function getUploadLocation(array $settings) {
  518     $destination = trim($settings['file_directory'], '/');
  519 
  520     // Replace tokens. As the tokens might contain HTML we convert it to plain
  521     // text.
  522     $destination = PlainTextOutput::renderFromHtml($this->token->replace($destination, []));
  523     return $settings['uri_scheme'] . '://' . $destination;
  524   }
  525 
  526   /**
  527    * Retrieves the upload validators for a field definition.
  528    *
  529    * This is copied from \Drupal\file\Plugin\Field\FieldType\FileItem as there
  530    * is no entity instance available here that a FileItem would exist for.
  531    *
  532    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
  533    *   The field definition for which to get validators.
  534    *
  535    * @return array
  536    *   An array suitable for passing to file_save_upload() or the file field
  537    *   element's '#upload_validators' property.
  538    */
  539   protected function getUploadValidators(FieldDefinitionInterface $field_definition) {
  540     $validators = [
  541       // Add in our check of the file name length.
  542       'file_validate_name_length' => [],
  543     ];
  544     $settings = $field_definition->getSettings();
  545 
  546     // Cap the upload size according to the PHP limit.
  547     $max_filesize = Bytes::toInt(Environment::getUploadMaxSize());
  548     if (!empty($settings['max_filesize'])) {
  549       $max_filesize = min($max_filesize, Bytes::toInt($settings['max_filesize']));
  550     }
  551 
  552     // There is always a file size limit due to the PHP server limit.
  553     $validators['file_validate_size'] = [$max_filesize];
  554 
  555     // Add the extension check if necessary.
  556     if (!empty($settings['file_extensions'])) {
  557       $validators['file_validate_extensions'] = [$settings['file_extensions']];
  558     }
  559 
  560     return $validators;
  561   }
  562 
  563   /**
  564    * {@inheritdoc}
  565    */
  566   protected function getBaseRoute($canonical_path, $method) {
  567     return new Route($canonical_path, [
  568       '_controller' => RequestHandler::class . '::handleRaw',
  569     ],
  570       $this->getBaseRouteRequirements($method),
  571       [],
  572       '',
  573       [],
  574       // The HTTP method is a requirement for this route.
  575       [$method]
  576     );
  577   }
  578 
  579   /**
  580    * {@inheritdoc}
  581    */
  582   protected function getBaseRouteRequirements($method) {
  583     $requirements = parent::getBaseRouteRequirements($method);
  584 
  585     // Add the content type format access check. This will enforce that all
  586     // incoming requests can only use the 'application/octet-stream'
  587     // Content-Type header.
  588     $requirements['_content_type_format'] = 'bin';
  589 
  590     return $requirements;
  591   }
  592 
  593   /**
  594    * Generates a lock ID based on the file URI.
  595    *
  596    * @param $file_uri
  597    *   The file URI.
  598    *
  599    * @return string
  600    *   The generated lock ID.
  601    */
  602   protected static function generateLockIdFromFileUri($file_uri) {
  603     return 'file:rest:' . Crypt::hashBase64($file_uri);
  604   }
  605 
  606 }