file.module 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813
  1. <?php
  2. /**
  3. * @file
  4. * Defines a "managed_file" Form API field and a "file" field for Field module.
  5. */
  6. use Drupal\Component\Utility\Environment;
  7. use Drupal\Core\Datetime\Entity\DateFormat;
  8. use Drupal\Core\Field\FieldDefinitionInterface;
  9. use Drupal\Core\File\Exception\FileException;
  10. use Drupal\Core\File\FileSystemInterface;
  11. use Drupal\Core\Form\FormStateInterface;
  12. use Drupal\Core\Messenger\MessengerInterface;
  13. use Drupal\Core\Render\BubbleableMetadata;
  14. use Drupal\Core\Render\Element;
  15. use Drupal\Core\Routing\RouteMatchInterface;
  16. use Drupal\Core\Link;
  17. use Drupal\Core\Url;
  18. use Drupal\file\Entity\File;
  19. use Drupal\file\FileInterface;
  20. use Drupal\Component\Utility\NestedArray;
  21. use Drupal\Component\Utility\Unicode;
  22. use Drupal\Core\Entity\EntityStorageInterface;
  23. use Drupal\Core\Template\Attribute;
  24. /**
  25. * The regex pattern used when checking for insecure file types.
  26. */
  27. define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');
  28. // Load all Field module hooks for File.
  29. require_once __DIR__ . '/file.field.inc';
  30. /**
  31. * Implements hook_help().
  32. */
  33. function file_help($route_name, RouteMatchInterface $route_match) {
  34. switch ($route_name) {
  35. case 'help.page.file':
  36. $output = '';
  37. $output .= '<h3>' . t('About') . '</h3>';
  38. $output .= '<p>' . t('The File module allows you to create fields that contain files. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":file_documentation">online documentation for the File module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':file_documentation' => 'https://www.drupal.org/documentation/modules/file']) . '</p>';
  39. $output .= '<h3>' . t('Uses') . '</h3>';
  40. $output .= '<dl>';
  41. $output .= '<dt>' . t('Managing and displaying file fields') . '</dt>';
  42. $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the file field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
  43. $output .= '<dt>' . t('Allowing file extensions') . '</dt>';
  44. $output .= '<dd>' . t('In the field settings, you can define the allowed file extensions (for example <em>pdf docx psd</em>) for the files that will be uploaded with the file field.') . '</dd>';
  45. $output .= '<dt>' . t('Storing files') . '</dt>';
  46. $output .= '<dd>' . t('Uploaded files can either be stored as <em>public</em> or <em>private</em>, depending on the <a href=":file-system">File system settings</a>. For more information, see the <a href=":system-help">System module help page</a>.', [':file-system' => Url::fromRoute('system.file_system_settings')->toString(), ':system-help' => Url::fromRoute('help.page', ['name' => 'system'])->toString()]) . '</dd>';
  47. $output .= '<dt>' . t('Restricting the maximum file size') . '</dt>';
  48. $output .= '<dd>' . t('The maximum file size that users can upload is limited by PHP settings of the server, but you can restrict by entering the desired value as the <em>Maximum upload size</em> setting. The maximum file size is automatically displayed to users in the help text of the file field.') . '</dd>';
  49. $output .= '<dt>' . t('Displaying files and descriptions') . '<dt>';
  50. $output .= '<dd>' . t('In the field settings, you can allow users to toggle whether individual files are displayed. In the display settings, you can then choose one of the following formats: <ul><li><em>Generic file</em> displays links to the files and adds icons that symbolize the file extensions. If <em>descriptions</em> are enabled and have been submitted, then the description is displayed instead of the file name.</li><li><em>URL to file</em> displays the full path to the file as plain text.</li><li><em>Table of files</em> lists links to the files and the file sizes in a table.</li><li><em>RSS enclosure</em> only displays the first file, and only in a RSS feed, formatted according to the RSS 2.0 syntax for enclosures.</li></ul> A file can still be linked to directly by its URI even if it is not displayed.') . '</dd>';
  51. $output .= '</dl>';
  52. return $output;
  53. }
  54. }
  55. /**
  56. * Implements hook_field_widget_info_alter().
  57. */
  58. function file_field_widget_info_alter(array &$info) {
  59. // Allows using the 'uri' widget for the 'file_uri' field type, which uses it
  60. // as the default widget.
  61. // @see \Drupal\file\Plugin\Field\FieldType\FileUriItem
  62. $info['uri']['field_types'][] = 'file_uri';
  63. }
  64. /**
  65. * Loads file entities from the database.
  66. *
  67. * @param array|null $fids
  68. * (optional) An array of entity IDs. If omitted or NULL, all entities are
  69. * loaded.
  70. * @param bool $reset
  71. * (optional) Whether to reset the internal file_load_multiple() cache.
  72. * Defaults to FALSE.
  73. *
  74. * @return array
  75. * An array of file entities, indexed by fid.
  76. *
  77. * @deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use
  78. * \Drupal\file\Entity\File::loadMultiple().
  79. *
  80. * @see https://www.drupal.org/node/2266845
  81. */
  82. function file_load_multiple(array $fids = NULL, $reset = FALSE) {
  83. @trigger_error('file_load_multiple() is deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use \Drupal\file\Entity\File::loadMultiple(). See https://www.drupal.org/node/2266845', E_USER_DEPRECATED);
  84. if ($reset) {
  85. \Drupal::entityTypeManager()->getStorage('file')->resetCache($fids);
  86. }
  87. return File::loadMultiple($fids);
  88. }
  89. /**
  90. * Loads a single file entity from the database.
  91. *
  92. * @param int $fid
  93. * A file ID.
  94. * @param bool $reset
  95. * (optional) Whether to reset the internal file_load_multiple() cache.
  96. * Defaults to FALSE.
  97. *
  98. * @return \Drupal\file\FileInterface|null
  99. * A file entity or NULL if the file was not found.
  100. *
  101. * @deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use
  102. * \Drupal\file\Entity\File::load().
  103. *
  104. * @see https://www.drupal.org/node/2266845
  105. */
  106. function file_load($fid, $reset = FALSE) {
  107. @trigger_error('file_load() is deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. Use \Drupal\file\Entity\File::load(). See https://www.drupal.org/node/2266845', E_USER_DEPRECATED);
  108. if ($reset) {
  109. \Drupal::entityTypeManager()->getStorage('file')->resetCache([$fid]);
  110. }
  111. return File::load($fid);
  112. }
  113. /**
  114. * Copies a file to a new location and adds a file record to the database.
  115. *
  116. * This function should be used when manipulating files that have records
  117. * stored in the database. This is a powerful function that in many ways
  118. * performs like an advanced version of copy().
  119. * - Checks if $source and $destination are valid and readable/writable.
  120. * - If file already exists in $destination either the call will error out,
  121. * replace the file or rename the file based on the $replace parameter.
  122. * - If the $source and $destination are equal, the behavior depends on the
  123. * $replace parameter. FileSystemInterface::EXISTS_REPLACE will error out.
  124. * FileSystemInterface::EXISTS_RENAME will rename the file until the
  125. * $destination is unique.
  126. * - Adds the new file to the files database. If the source file is a
  127. * temporary file, the resulting file will also be a temporary file. See
  128. * file_save_upload() for details on temporary files.
  129. *
  130. * @param \Drupal\file\FileInterface $source
  131. * A file entity.
  132. * @param string $destination
  133. * A string containing the destination that $source should be
  134. * copied to. This must be a stream wrapper URI.
  135. * @param int $replace
  136. * (optional) Replace behavior when the destination file already exists.
  137. * Possible values include:
  138. * - FileSystemInterface::EXISTS_REPLACE: Replace the existing file. If a
  139. * managed file with the destination name exists, then its database entry
  140. * will be updated. If no database entry is found, then a new one will be
  141. * created.
  142. * - FileSystemInterface::EXISTS_RENAME: (default) Append
  143. * _{incrementing number} until the filename is unique.
  144. * - FileSystemInterface::EXISTS_ERROR: Do nothing and return FALSE.
  145. *
  146. * @return \Drupal\file\FileInterface|false
  147. * File entity if the copy is successful, or FALSE in the event of an error.
  148. *
  149. * @see \Drupal\Core\File\FileSystemInterface::copy()
  150. * @see hook_file_copy()
  151. */
  152. function file_copy(FileInterface $source, $destination = NULL, $replace = FileSystemInterface::EXISTS_RENAME) {
  153. /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  154. $file_system = \Drupal::service('file_system');
  155. /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
  156. $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
  157. if (!$stream_wrapper_manager->isValidUri($destination)) {
  158. if (($realpath = $file_system->realpath($source->getFileUri())) !== FALSE) {
  159. \Drupal::logger('file')->notice('File %file (%realpath) could not be copied because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', ['%file' => $source->getFileUri(), '%realpath' => $realpath, '%destination' => $destination]);
  160. }
  161. else {
  162. \Drupal::logger('file')->notice('File %file could not be copied because the destination %destination is invalid. This is often caused by improper use of file_copy() or a missing stream wrapper.', ['%file' => $source->getFileUri(), '%destination' => $destination]);
  163. }
  164. \Drupal::messenger()->addError(t('The specified file %file could not be copied because the destination is invalid. More information is available in the system log.', ['%file' => $source->getFileUri()]));
  165. return FALSE;
  166. }
  167. try {
  168. $uri = $file_system->copy($source->getFileUri(), $destination, $replace);
  169. $file = $source->createDuplicate();
  170. $file->setFileUri($uri);
  171. $file->setFilename($file_system->basename($uri));
  172. // If we are replacing an existing file re-use its database record.
  173. // @todo Do not create a new entity in order to update it. See
  174. // https://www.drupal.org/node/2241865.
  175. if ($replace == FileSystemInterface::EXISTS_REPLACE) {
  176. $existing_files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri]);
  177. if (count($existing_files)) {
  178. $existing = reset($existing_files);
  179. $file->fid = $existing->id();
  180. $file->setOriginalId($existing->id());
  181. $file->setFilename($existing->getFilename());
  182. }
  183. }
  184. // If we are renaming around an existing file (rather than a directory),
  185. // use its basename for the filename.
  186. elseif ($replace == FileSystemInterface::EXISTS_RENAME && is_file($destination)) {
  187. $file->setFilename($file_system->basename($destination));
  188. }
  189. $file->save();
  190. // Inform modules that the file has been copied.
  191. \Drupal::moduleHandler()->invokeAll('file_copy', [$file, $source]);
  192. return $file;
  193. }
  194. catch (FileException $e) {
  195. return FALSE;
  196. }
  197. }
  198. /**
  199. * Moves a file to a new location and update the file's database entry.
  200. *
  201. * - Checks if $source and $destination are valid and readable/writable.
  202. * - Performs a file move if $source is not equal to $destination.
  203. * - If file already exists in $destination either the call will error out,
  204. * replace the file or rename the file based on the $replace parameter.
  205. * - Adds the new file to the files database.
  206. *
  207. * @param \Drupal\file\FileInterface $source
  208. * A file entity.
  209. * @param string $destination
  210. * A string containing the destination that $source should be moved
  211. * to. This must be a stream wrapper URI.
  212. * @param int $replace
  213. * (optional) The replace behavior when the destination file already exists.
  214. * Possible values include:
  215. * - FileSystemInterface::EXISTS_REPLACE: Replace the existing file. If a
  216. * managed file with the destination name exists then its database entry
  217. * will be updated and $source->delete() called after invoking
  218. * hook_file_move(). If no database entry is found, then the source files
  219. * record will be updated.
  220. * - FileSystemInterface::EXISTS_RENAME: (default) Append
  221. * _{incrementing number} until the filename is unique.
  222. * - FileSystemInterface::EXISTS_ERROR: Do nothing and return FALSE.
  223. *
  224. * @return \Drupal\file\FileInterface|false
  225. * Resulting file entity for success, or FALSE in the event of an error.
  226. *
  227. * @see \Drupal\Core\File\FileSystemInterface::move()
  228. * @see hook_file_move()
  229. */
  230. function file_move(FileInterface $source, $destination = NULL, $replace = FileSystemInterface::EXISTS_RENAME) {
  231. /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  232. $file_system = \Drupal::service('file_system');
  233. /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
  234. $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
  235. if (!$stream_wrapper_manager->isValidUri($destination)) {
  236. if (($realpath = $file_system->realpath($source->getFileUri())) !== FALSE) {
  237. \Drupal::logger('file')->notice('File %file (%realpath) could not be moved because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', ['%file' => $source->getFileUri(), '%realpath' => $realpath, '%destination' => $destination]);
  238. }
  239. else {
  240. \Drupal::logger('file')->notice('File %file could not be moved because the destination %destination is invalid. This may be caused by improper use of file_move() or a missing stream wrapper.', ['%file' => $source->getFileUri(), '%destination' => $destination]);
  241. }
  242. \Drupal::messenger()->addError(t('The specified file %file could not be moved because the destination is invalid. More information is available in the system log.', ['%file' => $source->getFileUri()]));
  243. return FALSE;
  244. }
  245. try {
  246. $uri = $file_system->move($source->getFileUri(), $destination, $replace);
  247. $delete_source = FALSE;
  248. $file = clone $source;
  249. $file->setFileUri($uri);
  250. // If we are replacing an existing file re-use its database record.
  251. if ($replace == FileSystemInterface::EXISTS_REPLACE) {
  252. $existing_files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri]);
  253. if (count($existing_files)) {
  254. $existing = reset($existing_files);
  255. $delete_source = TRUE;
  256. $file->fid = $existing->id();
  257. $file->uuid = $existing->uuid();
  258. }
  259. }
  260. // If we are renaming around an existing file (rather than a directory),
  261. // use its basename for the filename.
  262. elseif ($replace == FileSystemInterface::EXISTS_RENAME && is_file($destination)) {
  263. $file->setFilename(\Drupal::service('file_system')->basename($destination));
  264. }
  265. $file->save();
  266. // Inform modules that the file has been moved.
  267. \Drupal::moduleHandler()->invokeAll('file_move', [$file, $source]);
  268. // Delete the original if it's not in use elsewhere.
  269. if ($delete_source && !\Drupal::service('file.usage')->listUsage($source)) {
  270. $source->delete();
  271. }
  272. return $file;
  273. }
  274. catch (FileException $e) {
  275. return FALSE;
  276. }
  277. }
  278. /**
  279. * Checks that a file meets the criteria specified by the validators.
  280. *
  281. * After executing the validator callbacks specified hook_file_validate() will
  282. * also be called to allow other modules to report errors about the file.
  283. *
  284. * @param \Drupal\file\FileInterface $file
  285. * A file entity.
  286. * @param array $validators
  287. * (optional) An associative array of callback functions used to validate
  288. * the file. The keys are function names and the values arrays of callback
  289. * parameters which will be passed in after the file entity. The functions
  290. * should return an array of error messages; an empty array indicates that
  291. * the file passed validation. The callback functions will be called in the
  292. * order specified in the array, then the hook hook_file_validate()
  293. * will be invoked so other modules can validate the new file.
  294. *
  295. * @return array
  296. * An array containing validation error messages.
  297. *
  298. * @see hook_file_validate()
  299. */
  300. function file_validate(FileInterface $file, $validators = []) {
  301. // Call the validation functions specified by this function's caller.
  302. $errors = [];
  303. foreach ($validators as $function => $args) {
  304. if (function_exists($function)) {
  305. array_unshift($args, $file);
  306. $errors = array_merge($errors, call_user_func_array($function, $args));
  307. }
  308. }
  309. // Let other modules perform validation on the new file.
  310. return array_merge($errors, \Drupal::moduleHandler()->invokeAll('file_validate', [$file]));
  311. }
  312. /**
  313. * Checks for files with names longer than can be stored in the database.
  314. *
  315. * @param \Drupal\file\FileInterface $file
  316. * A file entity.
  317. *
  318. * @return array
  319. * An empty array if the file name length is smaller than the limit or an
  320. * array containing an error message if it's not or is empty.
  321. */
  322. function file_validate_name_length(FileInterface $file) {
  323. $errors = [];
  324. if (!$file->getFilename()) {
  325. $errors[] = t("The file's name is empty. Please give a name to the file.");
  326. }
  327. if (strlen($file->getFilename()) > 240) {
  328. $errors[] = t("The file's name exceeds the 240 characters limit. Please rename the file and try again.");
  329. }
  330. return $errors;
  331. }
  332. /**
  333. * Checks that the filename ends with an allowed extension.
  334. *
  335. * @param \Drupal\file\FileInterface $file
  336. * A file entity.
  337. * @param string $extensions
  338. * A string with a space separated list of allowed extensions.
  339. *
  340. * @return array
  341. * An empty array if the file extension is allowed or an array containing an
  342. * error message if it's not.
  343. *
  344. * @see hook_file_validate()
  345. */
  346. function file_validate_extensions(FileInterface $file, $extensions) {
  347. $errors = [];
  348. $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
  349. if (!preg_match($regex, $file->getFilename())) {
  350. $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', ['%files-allowed' => $extensions]);
  351. }
  352. return $errors;
  353. }
  354. /**
  355. * Checks that the file's size is below certain limits.
  356. *
  357. * @param \Drupal\file\FileInterface $file
  358. * A file entity.
  359. * @param int $file_limit
  360. * (optional) The maximum file size in bytes. Zero (the default) indicates
  361. * that no limit should be enforced.
  362. * @param int $user_limit
  363. * (optional) The maximum number of bytes the user is allowed. Zero (the
  364. * default) indicates that no limit should be enforced.
  365. *
  366. * @return array
  367. * An empty array if the file size is below limits or an array containing an
  368. * error message if it's not.
  369. *
  370. * @see hook_file_validate()
  371. */
  372. function file_validate_size(FileInterface $file, $file_limit = 0, $user_limit = 0) {
  373. $user = \Drupal::currentUser();
  374. $errors = [];
  375. if ($file_limit && $file->getSize() > $file_limit) {
  376. $errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', ['%filesize' => format_size($file->getSize()), '%maxsize' => format_size($file_limit)]);
  377. }
  378. // Save a query by only calling spaceUsed() when a limit is provided.
  379. if ($user_limit && (\Drupal::entityTypeManager()->getStorage('file')->spaceUsed($user->id()) + $file->getSize()) > $user_limit) {
  380. $errors[] = t('The file is %filesize which would exceed your disk quota of %quota.', ['%filesize' => format_size($file->getSize()), '%quota' => format_size($user_limit)]);
  381. }
  382. return $errors;
  383. }
  384. /**
  385. * Checks that the file is recognized as a valid image.
  386. *
  387. * @param \Drupal\file\FileInterface $file
  388. * A file entity.
  389. *
  390. * @return array
  391. * An empty array if the file is a valid image or an array containing an error
  392. * message if it's not.
  393. *
  394. * @see hook_file_validate()
  395. */
  396. function file_validate_is_image(FileInterface $file) {
  397. $errors = [];
  398. $image_factory = \Drupal::service('image.factory');
  399. $image = $image_factory->get($file->getFileUri());
  400. if (!$image->isValid()) {
  401. $supported_extensions = $image_factory->getSupportedExtensions();
  402. $errors[] = t('The image file is invalid or the image type is not allowed. Allowed types: %types', ['%types' => implode(', ', $supported_extensions)]);
  403. }
  404. return $errors;
  405. }
  406. /**
  407. * Verifies that image dimensions are within the specified maximum and minimum.
  408. *
  409. * Non-image files will be ignored. If an image toolkit is available the image
  410. * will be scaled to fit within the desired maximum dimensions.
  411. *
  412. * @param \Drupal\file\FileInterface $file
  413. * A file entity. This function may resize the file affecting its size.
  414. * @param string|int $maximum_dimensions
  415. * (optional) A string in the form WIDTHxHEIGHT; for example, '640x480' or
  416. * '85x85'. If an image toolkit is installed, the image will be resized down
  417. * to these dimensions. A value of zero (the default) indicates no restriction
  418. * on size, so no resizing will be attempted.
  419. * @param string|int $minimum_dimensions
  420. * (optional) A string in the form WIDTHxHEIGHT. This will check that the
  421. * image meets a minimum size. A value of zero (the default) indicates that
  422. * there is no restriction on size.
  423. *
  424. * @return array
  425. * An empty array if the file meets the specified dimensions, was resized
  426. * successfully to meet those requirements or is not an image. If the image
  427. * does not meet the requirements or an attempt to resize it fails, an array
  428. * containing the error message will be returned.
  429. *
  430. * @see hook_file_validate()
  431. */
  432. function file_validate_image_resolution(FileInterface $file, $maximum_dimensions = 0, $minimum_dimensions = 0) {
  433. $errors = [];
  434. // Check first that the file is an image.
  435. $image_factory = \Drupal::service('image.factory');
  436. $image = $image_factory->get($file->getFileUri());
  437. if ($image->isValid()) {
  438. $scaling = FALSE;
  439. if ($maximum_dimensions) {
  440. // Check that it is smaller than the given dimensions.
  441. list($width, $height) = explode('x', $maximum_dimensions);
  442. if ($image->getWidth() > $width || $image->getHeight() > $height) {
  443. // Try to resize the image to fit the dimensions.
  444. if ($image->scale($width, $height)) {
  445. $scaling = TRUE;
  446. $image->save();
  447. if (!empty($width) && !empty($height)) {
  448. $message = t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels. The new dimensions of the resized image are %new_widthx%new_height pixels.',
  449. [
  450. '%dimensions' => $maximum_dimensions,
  451. '%new_width' => $image->getWidth(),
  452. '%new_height' => $image->getHeight(),
  453. ]);
  454. }
  455. elseif (empty($width)) {
  456. $message = t('The image was resized to fit within the maximum allowed height of %height pixels. The new dimensions of the resized image are %new_widthx%new_height pixels.',
  457. [
  458. '%height' => $height,
  459. '%new_width' => $image->getWidth(),
  460. '%new_height' => $image->getHeight(),
  461. ]);
  462. }
  463. elseif (empty($height)) {
  464. $message = t('The image was resized to fit within the maximum allowed width of %width pixels. The new dimensions of the resized image are %new_widthx%new_height pixels.',
  465. [
  466. '%width' => $width,
  467. '%new_width' => $image->getWidth(),
  468. '%new_height' => $image->getHeight(),
  469. ]);
  470. }
  471. \Drupal::messenger()->addStatus($message);
  472. }
  473. else {
  474. $errors[] = t('The image exceeds the maximum allowed dimensions and an attempt to resize it failed.');
  475. }
  476. }
  477. }
  478. if ($minimum_dimensions) {
  479. // Check that it is larger than the given dimensions.
  480. list($width, $height) = explode('x', $minimum_dimensions);
  481. if ($image->getWidth() < $width || $image->getHeight() < $height) {
  482. if ($scaling) {
  483. $errors[] = t('The resized image is too small. The minimum dimensions are %dimensions pixels and after resizing, the image size will be %widthx%height pixels.',
  484. [
  485. '%dimensions' => $minimum_dimensions,
  486. '%width' => $image->getWidth(),
  487. '%height' => $image->getHeight(),
  488. ]);
  489. }
  490. else {
  491. $errors[] = t('The image is too small. The minimum dimensions are %dimensions pixels and the image size is %widthx%height pixels.',
  492. [
  493. '%dimensions' => $minimum_dimensions,
  494. '%width' => $image->getWidth(),
  495. '%height' => $image->getHeight(),
  496. ]);
  497. }
  498. }
  499. }
  500. }
  501. return $errors;
  502. }
  503. /**
  504. * Saves a file to the specified destination and creates a database entry.
  505. *
  506. * @param string $data
  507. * A string containing the contents of the file.
  508. * @param string|null $destination
  509. * (optional) A string containing the destination URI. This must be a stream
  510. * wrapper URI. If no value or NULL is provided, a randomized name will be
  511. * generated and the file will be saved using Drupal's default files scheme,
  512. * usually "public://".
  513. * @param int $replace
  514. * (optional) The replace behavior when the destination file already exists.
  515. * Possible values include:
  516. * - FileSystemInterface::EXISTS_REPLACE: Replace the existing file. If a
  517. * managed file with the destination name exists, then its database entry
  518. * will be updated. If no database entry is found, then a new one will be
  519. * created.
  520. * - FileSystemInterface::EXISTS_RENAME: (default) Append
  521. * _{incrementing number} until the filename is unique.
  522. * - FileSystemInterface::EXISTS_ERROR: Do nothing and return FALSE.
  523. *
  524. * @return \Drupal\file\FileInterface|false
  525. * A file entity, or FALSE on error.
  526. *
  527. * @see \Drupal\Core\File\FileSystemInterface::saveData()
  528. */
  529. function file_save_data($data, $destination = NULL, $replace = FileSystemInterface::EXISTS_RENAME) {
  530. $user = \Drupal::currentUser();
  531. if (empty($destination)) {
  532. $destination = \Drupal::config('system.file')->get('default_scheme') . '://';
  533. }
  534. /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
  535. $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
  536. if (!$stream_wrapper_manager->isValidUri($destination)) {
  537. \Drupal::logger('file')->notice('The data could not be saved because the destination %destination is invalid. This may be caused by improper use of file_save_data() or a missing stream wrapper.', ['%destination' => $destination]);
  538. \Drupal::messenger()->addError(t('The data could not be saved because the destination is invalid. More information is available in the system log.'));
  539. return FALSE;
  540. }
  541. try {
  542. $uri = \Drupal::service('file_system')->saveData($data, $destination, $replace);
  543. // Create a file entity.
  544. $file = File::create([
  545. 'uri' => $uri,
  546. 'uid' => $user->id(),
  547. 'status' => FILE_STATUS_PERMANENT,
  548. ]);
  549. // If we are replacing an existing file re-use its database record.
  550. // @todo Do not create a new entity in order to update it. See
  551. // https://www.drupal.org/node/2241865.
  552. if ($replace == FileSystemInterface::EXISTS_REPLACE) {
  553. $existing_files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri]);
  554. if (count($existing_files)) {
  555. $existing = reset($existing_files);
  556. $file->fid = $existing->id();
  557. $file->setOriginalId($existing->id());
  558. $file->setFilename($existing->getFilename());
  559. }
  560. }
  561. // If we are renaming around an existing file (rather than a directory),
  562. // use its basename for the filename.
  563. elseif ($replace == FileSystemInterface::EXISTS_RENAME && is_file($destination)) {
  564. $file->setFilename(\Drupal::service('file_system')->basename($destination));
  565. }
  566. $file->save();
  567. return $file;
  568. }
  569. catch (FileException $e) {
  570. return FALSE;
  571. }
  572. }
  573. /**
  574. * Examines a file entity and returns appropriate content headers for download.
  575. *
  576. * @param \Drupal\file\FileInterface $file
  577. * A file entity.
  578. *
  579. * @return array
  580. * An associative array of headers, as expected by
  581. * \Symfony\Component\HttpFoundation\StreamedResponse.
  582. */
  583. function file_get_content_headers(FileInterface $file) {
  584. $type = Unicode::mimeHeaderEncode($file->getMimeType());
  585. return [
  586. 'Content-Type' => $type,
  587. 'Content-Length' => $file->getSize(),
  588. 'Cache-Control' => 'private',
  589. ];
  590. }
  591. /**
  592. * Implements hook_theme().
  593. */
  594. function file_theme() {
  595. return [
  596. // From file.module.
  597. 'file_link' => [
  598. 'variables' => ['file' => NULL, 'description' => NULL, 'attributes' => []],
  599. ],
  600. 'file_managed_file' => [
  601. 'render element' => 'element',
  602. ],
  603. 'file_audio' => [
  604. 'variables' => ['files' => [], 'attributes' => NULL],
  605. ],
  606. 'file_video' => [
  607. 'variables' => ['files' => [], 'attributes' => NULL],
  608. ],
  609. // From file.field.inc.
  610. 'file_widget_multiple' => [
  611. 'render element' => 'element',
  612. 'file' => 'file.field.inc',
  613. ],
  614. 'file_upload_help' => [
  615. 'variables' => ['description' => NULL, 'upload_validators' => NULL, 'cardinality' => NULL],
  616. 'file' => 'file.field.inc',
  617. ],
  618. ];
  619. }
  620. /**
  621. * Implements hook_file_download().
  622. */
  623. function file_file_download($uri) {
  624. // Get the file record based on the URI. If not in the database just return.
  625. /** @var \Drupal\file\FileInterface[] $files */
  626. $files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri]);
  627. if (count($files)) {
  628. foreach ($files as $item) {
  629. // Since some database servers sometimes use a case-insensitive comparison
  630. // by default, double check that the filename is an exact match.
  631. if ($item->getFileUri() === $uri) {
  632. $file = $item;
  633. break;
  634. }
  635. }
  636. }
  637. if (!isset($file)) {
  638. return;
  639. }
  640. // Find out if a temporary file is still used in the system.
  641. if ($file->isTemporary()) {
  642. $usage = \Drupal::service('file.usage')->listUsage($file);
  643. if (empty($usage) && $file->getOwnerId() != \Drupal::currentUser()->id()) {
  644. // Deny access to temporary files without usage that are not owned by the
  645. // same user. This prevents the security issue that a private file that
  646. // was protected by field permissions becomes available after its usage
  647. // was removed and before it is actually deleted from the file system.
  648. // Modules that depend on this behavior should make the file permanent
  649. // instead.
  650. return -1;
  651. }
  652. }
  653. // Find out which (if any) fields of this type contain the file.
  654. $references = file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_CURRENT, NULL);
  655. // Stop processing if there are no references in order to avoid returning
  656. // headers for files controlled by other modules. Make an exception for
  657. // temporary files where the host entity has not yet been saved (for example,
  658. // an image preview on a node/add form) in which case, allow download by the
  659. // file's owner.
  660. if (empty($references) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
  661. return;
  662. }
  663. if (!$file->access('download')) {
  664. return -1;
  665. }
  666. // Access is granted.
  667. $headers = file_get_content_headers($file);
  668. return $headers;
  669. }
  670. /**
  671. * Implements hook_cron().
  672. */
  673. function file_cron() {
  674. $age = \Drupal::config('system.file')->get('temporary_maximum_age');
  675. $file_storage = \Drupal::entityTypeManager()->getStorage('file');
  676. /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
  677. $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
  678. // Only delete temporary files if older than $age. Note that automatic cleanup
  679. // is disabled if $age set to 0.
  680. if ($age) {
  681. $fids = Drupal::entityQuery('file')
  682. ->condition('status', FILE_STATUS_PERMANENT, '<>')
  683. ->condition('changed', REQUEST_TIME - $age, '<')
  684. ->range(0, 100)
  685. ->execute();
  686. $files = $file_storage->loadMultiple($fids);
  687. foreach ($files as $file) {
  688. $references = \Drupal::service('file.usage')->listUsage($file);
  689. if (empty($references)) {
  690. if (!file_exists($file->getFileUri())) {
  691. if (!$stream_wrapper_manager->isValidUri($file->getFileUri())) {
  692. \Drupal::logger('file system')->warning('Temporary file "%path" that was deleted during garbage collection did not exist on the filesystem. This could be caused by a missing stream wrapper.', ['%path' => $file->getFileUri()]);
  693. }
  694. else {
  695. \Drupal::logger('file system')->warning('Temporary file "%path" that was deleted during garbage collection did not exist on the filesystem.', ['%path' => $file->getFileUri()]);
  696. }
  697. }
  698. // Delete the file entity. If the file does not exist, this will
  699. // generate a second notice in the watchdog.
  700. $file->delete();
  701. }
  702. else {
  703. \Drupal::logger('file system')->info('Did not delete temporary file "%path" during garbage collection because it is in use by the following modules: %modules.', ['%path' => $file->getFileUri(), '%modules' => implode(', ', array_keys($references))]);
  704. }
  705. }
  706. }
  707. }
  708. /**
  709. * Saves form file uploads.
  710. *
  711. * The files will be added to the {file_managed} table as temporary files.
  712. * Temporary files are periodically cleaned. Use the 'file.usage' service to
  713. * register the usage of the file which will automatically mark it as permanent.
  714. *
  715. * @param array $element
  716. * The FAPI element whose values are being saved.
  717. * @param \Drupal\Core\Form\FormStateInterface $form_state
  718. * The current state of the form.
  719. * @param null|int $delta
  720. * (optional) The delta of the file to return the file entity.
  721. * Defaults to NULL.
  722. * @param int $replace
  723. * (optional) The replace behavior when the destination file already exists.
  724. * Possible values include:
  725. * - FileSystemInterface::EXISTS_REPLACE: Replace the existing file.
  726. * - FileSystemInterface::EXISTS_RENAME: (default) Append
  727. * _{incrementing number} until the filename is unique.
  728. * - FileSystemInterface::EXISTS_ERROR: Do nothing and return FALSE.
  729. *
  730. * @return array|\Drupal\file\FileInterface|null|false
  731. * An array of file entities or a single file entity if $delta != NULL. Each
  732. * array element contains the file entity if the upload succeeded or FALSE if
  733. * there was an error. Function returns NULL if no file was uploaded.
  734. *
  735. * @internal
  736. * This function is internal, and may be removed in a minor version release.
  737. * It wraps file_save_upload() to allow correct error handling in forms.
  738. * Contrib and custom code should not call this function, they should use the
  739. * managed file upload widgets in core.
  740. *
  741. * @see https://www.drupal.org/project/drupal/issues/3069020
  742. * @see https://www.drupal.org/project/drupal/issues/2482783
  743. */
  744. function _file_save_upload_from_form(array $element, FormStateInterface $form_state, $delta = NULL, $replace = FileSystemInterface::EXISTS_RENAME) {
  745. // Get all errors set before calling this method. This will also clear them
  746. // from $_SESSION.
  747. $errors_before = \Drupal::messenger()->deleteByType(MessengerInterface::TYPE_ERROR);
  748. $upload_location = isset($element['#upload_location']) ? $element['#upload_location'] : FALSE;
  749. $upload_name = implode('_', $element['#parents']);
  750. $upload_validators = isset($element['#upload_validators']) ? $element['#upload_validators'] : [];
  751. $result = file_save_upload($upload_name, $upload_validators, $upload_location, $delta, $replace);
  752. // Get new errors that are generated while trying to save the upload. This
  753. // will also clear them from $_SESSION.
  754. $errors_new = \Drupal::messenger()->deleteByType(MessengerInterface::TYPE_ERROR);
  755. if (!empty($errors_new)) {
  756. if (count($errors_new) > 1) {
  757. // Render multiple errors into a single message.
  758. // This is needed because only one error per element is supported.
  759. $render_array = [
  760. 'error' => [
  761. '#markup' => t('One or more files could not be uploaded.'),
  762. ],
  763. 'item_list' => [
  764. '#theme' => 'item_list',
  765. '#items' => $errors_new,
  766. ],
  767. ];
  768. $error_message = \Drupal::service('renderer')->renderPlain($render_array);
  769. }
  770. else {
  771. $error_message = reset($errors_new);
  772. }
  773. $form_state->setError($element, $error_message);
  774. }
  775. // Ensure that errors set prior to calling this method are still shown to the
  776. // user.
  777. if (!empty($errors_before)) {
  778. foreach ($errors_before as $error) {
  779. \Drupal::messenger()->addError($error);
  780. }
  781. }
  782. return $result;
  783. }
  784. /**
  785. * Saves file uploads to a new location.
  786. *
  787. * The files will be added to the {file_managed} table as temporary files.
  788. * Temporary files are periodically cleaned. Use the 'file.usage' service to
  789. * register the usage of the file which will automatically mark it as permanent.
  790. *
  791. * Note that this function does not support correct form error handling. The
  792. * file upload widgets in core do support this. It is advised to use these in
  793. * any custom form, instead of calling this function.
  794. *
  795. * @param string $form_field_name
  796. * A string that is the associative array key of the upload form element in
  797. * the form array.
  798. * @param array $validators
  799. * (optional) An associative array of callback functions used to validate the
  800. * file. See file_validate() for a full discussion of the array format.
  801. * If the array is empty, it will be set up to call file_validate_extensions()
  802. * with a safe list of extensions, as follows: "jpg jpeg gif png txt doc
  803. * xls pdf ppt pps odt ods odp". To allow all extensions, you must explicitly
  804. * set this array to ['file_validate_extensions' => '']. (Beware: this is not
  805. * safe and should only be allowed for trusted users, if at all.)
  806. * @param string|false $destination
  807. * (optional) A string containing the URI that the file should be copied to.
  808. * This must be a stream wrapper URI. If this value is omitted or set to
  809. * FALSE, Drupal's temporary files scheme will be used ("temporary://").
  810. * @param null|int $delta
  811. * (optional) The delta of the file to return the file entity.
  812. * Defaults to NULL.
  813. * @param int $replace
  814. * (optional) The replace behavior when the destination file already exists.
  815. * Possible values include:
  816. * - FileSystemInterface::EXISTS_REPLACE: Replace the existing file.
  817. * - FileSystemInterface::EXISTS_RENAME: (default) Append
  818. * _{incrementing number} until the filename is unique.
  819. * - FileSystemInterface::EXISTS_ERROR: Do nothing and return FALSE.
  820. *
  821. * @return array|\Drupal\file\FileInterface|null|false
  822. * An array of file entities or a single file entity if $delta != NULL. Each
  823. * array element contains the file entity if the upload succeeded or FALSE if
  824. * there was an error. Function returns NULL if no file was uploaded.
  825. *
  826. * @see _file_save_upload_from_form()
  827. *
  828. * @todo: move this logic to a service in https://www.drupal.org/node/2244513.
  829. */
  830. function file_save_upload($form_field_name, $validators = [], $destination = FALSE, $delta = NULL, $replace = FileSystemInterface::EXISTS_RENAME) {
  831. static $upload_cache;
  832. $all_files = \Drupal::request()->files->get('files', []);
  833. // Make sure there's an upload to process.
  834. if (empty($all_files[$form_field_name])) {
  835. return NULL;
  836. }
  837. $file_upload = $all_files[$form_field_name];
  838. // Return cached objects without processing since the file will have
  839. // already been processed and the paths in $_FILES will be invalid.
  840. if (isset($upload_cache[$form_field_name])) {
  841. if (isset($delta)) {
  842. return $upload_cache[$form_field_name][$delta];
  843. }
  844. return $upload_cache[$form_field_name];
  845. }
  846. // Prepare uploaded files info. Representation is slightly different
  847. // for multiple uploads and we fix that here.
  848. $uploaded_files = $file_upload;
  849. if (!is_array($file_upload)) {
  850. $uploaded_files = [$file_upload];
  851. }
  852. $files = [];
  853. foreach ($uploaded_files as $i => $file_info) {
  854. $files[$i] = _file_save_upload_single($file_info, $form_field_name, $validators, $destination, $replace);
  855. }
  856. // Add files to the cache.
  857. $upload_cache[$form_field_name] = $files;
  858. return isset($delta) ? $files[$delta] : $files;
  859. }
  860. /**
  861. * Saves a file upload to a new location.
  862. *
  863. * @param \SplFileInfo $file_info
  864. * The file upload to save.
  865. * @param string $form_field_name
  866. * A string that is the associative array key of the upload form element in
  867. * the form array.
  868. * @param array $validators
  869. * (optional) An associative array of callback functions used to validate the
  870. * file.
  871. * @param bool $destination
  872. * (optional) A string containing the URI that the file should be copied to.
  873. * @param int $replace
  874. * (optional) The replace behavior when the destination file already exists.
  875. *
  876. * @return \Drupal\file\FileInterface|false
  877. * The created file entity or FALSE if the uploaded file not saved.
  878. *
  879. * @throws \Drupal\Core\Entity\EntityStorageException
  880. *
  881. * @internal
  882. * This method should only be called from file_save_upload(). Use that method
  883. * instead.
  884. *
  885. * @see file_save_upload()
  886. */
  887. function _file_save_upload_single(\SplFileInfo $file_info, $form_field_name, $validators = [], $destination = FALSE, $replace = FileSystemInterface::EXISTS_REPLACE) {
  888. $user = \Drupal::currentUser();
  889. // Check for file upload errors and return FALSE for this file if a lower
  890. // level system error occurred. For a complete list of errors:
  891. // See http://php.net/manual/features.file-upload.errors.php.
  892. switch ($file_info->getError()) {
  893. case UPLOAD_ERR_INI_SIZE:
  894. case UPLOAD_ERR_FORM_SIZE:
  895. \Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_info->getFilename(), '%maxsize' => format_size(Environment::getUploadMaxSize())]));
  896. return FALSE;
  897. case UPLOAD_ERR_PARTIAL:
  898. case UPLOAD_ERR_NO_FILE:
  899. \Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]));
  900. return FALSE;
  901. case UPLOAD_ERR_OK:
  902. // Final check that this is a valid upload, if it isn't, use the
  903. // default error handler.
  904. if (is_uploaded_file($file_info->getRealPath())) {
  905. break;
  906. }
  907. default:
  908. // Unknown error
  909. \Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]));
  910. return FALSE;
  911. }
  912. // Begin building file entity.
  913. $values = [
  914. 'uid' => $user->id(),
  915. 'status' => 0,
  916. 'filename' => trim($file_info->getClientOriginalName(), '.'),
  917. 'uri' => $file_info->getRealPath(),
  918. 'filesize' => $file_info->getSize(),
  919. ];
  920. $values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
  921. $file = File::create($values);
  922. $extensions = '';
  923. if (isset($validators['file_validate_extensions'])) {
  924. if (isset($validators['file_validate_extensions'][0])) {
  925. // Build the list of non-munged extensions if the caller provided them.
  926. $extensions = $validators['file_validate_extensions'][0];
  927. }
  928. else {
  929. // If 'file_validate_extensions' is set and the list is empty then the
  930. // caller wants to allow any extension. In this case we have to remove the
  931. // validator or else it will reject all extensions.
  932. unset($validators['file_validate_extensions']);
  933. }
  934. }
  935. else {
  936. // No validator was provided, so add one using the default list.
  937. // Build a default non-munged safe list for file_munge_filename().
  938. $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
  939. $validators['file_validate_extensions'] = [];
  940. $validators['file_validate_extensions'][0] = $extensions;
  941. }
  942. if (!empty($extensions)) {
  943. // Munge the filename to protect against possible malicious extension
  944. // hiding within an unknown file type (ie: filename.html.foo).
  945. $file->setFilename(file_munge_filename($file->getFilename(), $extensions));
  946. }
  947. // Rename potentially executable files, to help prevent exploits (i.e. will
  948. // rename filename.php.foo and filename.php to filename.php.foo.txt and
  949. // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
  950. // evaluates to TRUE.
  951. if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
  952. $file->setMimeType('text/plain');
  953. // The destination filename will also later be used to create the URI.
  954. $file->setFilename($file->getFilename() . '.txt');
  955. // The .txt extension may not be in the allowed list of extensions. We have
  956. // to add it here or else the file upload will fail.
  957. if (!empty($extensions)) {
  958. $validators['file_validate_extensions'][0] .= ' txt';
  959. \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
  960. }
  961. }
  962. // If the destination is not provided, use the temporary directory.
  963. if (empty($destination)) {
  964. $destination = 'temporary://';
  965. }
  966. /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
  967. $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
  968. // Assert that the destination contains a valid stream.
  969. $destination_scheme = $stream_wrapper_manager::getScheme($destination);
  970. if (!$stream_wrapper_manager->isValidScheme($destination_scheme)) {
  971. \Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
  972. return FALSE;
  973. }
  974. $file->source = $form_field_name;
  975. // A file URI may already have a trailing slash or look like "public://".
  976. if (substr($destination, -1) != '/') {
  977. $destination .= '/';
  978. }
  979. /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  980. $file_system = \Drupal::service('file_system');
  981. try {
  982. $file->destination = $file_system->getDestinationFilename($destination . $file->getFilename(), $replace);
  983. }
  984. catch (FileException $e) {
  985. \Drupal::messenger()->addError(t('The file %filename could not be uploaded because the name is invalid.', ['%filename' => $file->getFilename()]));
  986. return FALSE;
  987. }
  988. // If the destination is FALSE then there is an existing file and $replace is
  989. // set to return an error, so we need to exit.
  990. if ($file->destination === FALSE) {
  991. \Drupal::messenger()->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', ['%source' => $form_field_name, '%directory' => $destination]));
  992. return FALSE;
  993. }
  994. // Add in our check of the file name length.
  995. $validators['file_validate_name_length'] = [];
  996. // Call the validation functions specified by this function's caller.
  997. $errors = file_validate($file, $validators);
  998. // Check for errors.
  999. if (!empty($errors)) {
  1000. $message = [
  1001. 'error' => [
  1002. '#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
  1003. ],
  1004. 'item_list' => [
  1005. '#theme' => 'item_list',
  1006. '#items' => $errors,
  1007. ],
  1008. ];
  1009. // @todo Add support for render arrays in
  1010. // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
  1011. // @see https://www.drupal.org/node/2505497.
  1012. \Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
  1013. return FALSE;
  1014. }
  1015. $file->setFileUri($file->destination);
  1016. if (!$file_system->moveUploadedFile($file_info->getRealPath(), $file->getFileUri())) {
  1017. \Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
  1018. \Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
  1019. return FALSE;
  1020. }
  1021. // Set the permissions on the new file.
  1022. $file_system->chmod($file->getFileUri());
  1023. // If we are replacing an existing file re-use its database record.
  1024. // @todo Do not create a new entity in order to update it. See
  1025. // https://www.drupal.org/node/2241865.
  1026. if ($replace == FileSystemInterface::EXISTS_REPLACE) {
  1027. $existing_files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $file->getFileUri()]);
  1028. if (count($existing_files)) {
  1029. $existing = reset($existing_files);
  1030. $file->fid = $existing->id();
  1031. $file->setOriginalId($existing->id());
  1032. }
  1033. }
  1034. // Update the filename with any changes as a result of security or renaming
  1035. // due to an existing file.
  1036. $file->setFilename(\Drupal::service('file_system')->basename($file->destination));
  1037. // We can now validate the file object itself before it's saved.
  1038. $violations = $file->validate();
  1039. foreach ($violations as $violation) {
  1040. $errors[] = $violation->getMessage();
  1041. }
  1042. if (!empty($errors)) {
  1043. $message = [
  1044. 'error' => [
  1045. '#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
  1046. ],
  1047. 'item_list' => [
  1048. '#theme' => 'item_list',
  1049. '#items' => $errors,
  1050. ],
  1051. ];
  1052. // @todo Add support for render arrays in
  1053. // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
  1054. // @see https://www.drupal.org/node/2505497.
  1055. \Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
  1056. return FALSE;
  1057. }
  1058. // If we made it this far it's safe to record this file in the database.
  1059. $file->save();
  1060. // Allow an anonymous user who creates a non-public file to see it. See
  1061. // \Drupal\file\FileAccessControlHandler::checkAccess().
  1062. if ($user->isAnonymous() && $destination_scheme !== 'public') {
  1063. $session = \Drupal::request()->getSession();
  1064. $allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
  1065. $allowed_temp_files[$file->id()] = $file->id();
  1066. $session->set('anonymous_allowed_file_ids', $allowed_temp_files);
  1067. }
  1068. return $file;
  1069. }
  1070. /**
  1071. * Determines the preferred upload progress implementation.
  1072. *
  1073. * @return string|false
  1074. * A string indicating which upload progress system is available. Either "apc"
  1075. * or "uploadprogress". If neither are available, returns FALSE.
  1076. */
  1077. function file_progress_implementation() {
  1078. static $implementation;
  1079. if (!isset($implementation)) {
  1080. $implementation = FALSE;
  1081. // We prefer the PECL extension uploadprogress because it supports multiple
  1082. // simultaneous uploads. APCu only supports one at a time.
  1083. if (extension_loaded('uploadprogress')) {
  1084. $implementation = 'uploadprogress';
  1085. }
  1086. }
  1087. return $implementation;
  1088. }
  1089. /**
  1090. * Implements hook_ENTITY_TYPE_predelete() for file entities.
  1091. */
  1092. function file_file_predelete(File $file) {
  1093. // @todo Remove references to a file that is in-use.
  1094. }
  1095. /**
  1096. * Implements hook_tokens().
  1097. */
  1098. function file_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
  1099. $token_service = \Drupal::token();
  1100. $url_options = ['absolute' => TRUE];
  1101. if (isset($options['langcode'])) {
  1102. $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
  1103. $langcode = $options['langcode'];
  1104. }
  1105. else {
  1106. $langcode = NULL;
  1107. }
  1108. $replacements = [];
  1109. if ($type == 'file' && !empty($data['file'])) {
  1110. /** @var \Drupal\file\FileInterface $file */
  1111. $file = $data['file'];
  1112. foreach ($tokens as $name => $original) {
  1113. switch ($name) {
  1114. // Basic keys and values.
  1115. case 'fid':
  1116. $replacements[$original] = $file->id();
  1117. break;
  1118. // Essential file data
  1119. case 'name':
  1120. $replacements[$original] = $file->getFilename();
  1121. break;
  1122. case 'path':
  1123. $replacements[$original] = $file->getFileUri();
  1124. break;
  1125. case 'mime':
  1126. $replacements[$original] = $file->getMimeType();
  1127. break;
  1128. case 'size':
  1129. $replacements[$original] = format_size($file->getSize());
  1130. break;
  1131. case 'url':
  1132. // Ideally, this would use file_url_transform_relative(), but because
  1133. // tokens are also often used in e-mails, it's better to keep absolute
  1134. // file URLs. The 'url.site' cache context is associated to ensure the
  1135. // correct absolute URL is used in case of a multisite setup.
  1136. $replacements[$original] = $file->createFileUrl(FALSE);
  1137. $bubbleable_metadata->addCacheContexts(['url.site']);
  1138. break;
  1139. // These tokens are default variations on the chained tokens handled below.
  1140. case 'created':
  1141. $date_format = DateFormat::load('medium');
  1142. $bubbleable_metadata->addCacheableDependency($date_format);
  1143. $replacements[$original] = \Drupal::service('date.formatter')->format($file->getCreatedTime(), 'medium', '', NULL, $langcode);
  1144. break;
  1145. case 'changed':
  1146. $date_format = DateFormat::load('medium');
  1147. $bubbleable_metadata = $bubbleable_metadata->addCacheableDependency($date_format);
  1148. $replacements[$original] = \Drupal::service('date.formatter')->format($file->getChangedTime(), 'medium', '', NULL, $langcode);
  1149. break;
  1150. case 'owner':
  1151. $owner = $file->getOwner();
  1152. $bubbleable_metadata->addCacheableDependency($owner);
  1153. $name = $owner->label();
  1154. $replacements[$original] = $name;
  1155. break;
  1156. }
  1157. }
  1158. if ($date_tokens = $token_service->findWithPrefix($tokens, 'created')) {
  1159. $replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getCreatedTime()], $options, $bubbleable_metadata);
  1160. }
  1161. if ($date_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
  1162. $replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getChangedTime()], $options, $bubbleable_metadata);
  1163. }
  1164. if (($owner_tokens = $token_service->findWithPrefix($tokens, 'owner')) && $file->getOwner()) {
  1165. $replacements += $token_service->generate('user', $owner_tokens, ['user' => $file->getOwner()], $options, $bubbleable_metadata);
  1166. }
  1167. }
  1168. return $replacements;
  1169. }
  1170. /**
  1171. * Implements hook_token_info().
  1172. */
  1173. function file_token_info() {
  1174. $types['file'] = [
  1175. 'name' => t("Files"),
  1176. 'description' => t("Tokens related to uploaded files."),
  1177. 'needs-data' => 'file',
  1178. ];
  1179. // File related tokens.
  1180. $file['fid'] = [
  1181. 'name' => t("File ID"),
  1182. 'description' => t("The unique ID of the uploaded file."),
  1183. ];
  1184. $file['name'] = [
  1185. 'name' => t("File name"),
  1186. 'description' => t("The name of the file on disk."),
  1187. ];
  1188. $file['path'] = [
  1189. 'name' => t("Path"),
  1190. 'description' => t("The location of the file relative to Drupal root."),
  1191. ];
  1192. $file['mime'] = [
  1193. 'name' => t("MIME type"),
  1194. 'description' => t("The MIME type of the file."),
  1195. ];
  1196. $file['size'] = [
  1197. 'name' => t("File size"),
  1198. 'description' => t("The size of the file."),
  1199. ];
  1200. $file['url'] = [
  1201. 'name' => t("URL"),
  1202. 'description' => t("The web-accessible URL for the file."),
  1203. ];
  1204. $file['created'] = [
  1205. 'name' => t("Created"),
  1206. 'description' => t("The date the file created."),
  1207. 'type' => 'date',
  1208. ];
  1209. $file['changed'] = [
  1210. 'name' => t("Changed"),
  1211. 'description' => t("The date the file was most recently changed."),
  1212. 'type' => 'date',
  1213. ];
  1214. $file['owner'] = [
  1215. 'name' => t("Owner"),
  1216. 'description' => t("The user who originally uploaded the file."),
  1217. 'type' => 'user',
  1218. ];
  1219. return [
  1220. 'types' => $types,
  1221. 'tokens' => [
  1222. 'file' => $file,
  1223. ],
  1224. ];
  1225. }
  1226. /**
  1227. * Form submission handler for upload / remove buttons of managed_file elements.
  1228. *
  1229. * @see \Drupal\file\Element\ManagedFile::processManagedFile()
  1230. */
  1231. function file_managed_file_submit($form, FormStateInterface $form_state) {
  1232. // Determine whether it was the upload or the remove button that was clicked,
  1233. // and set $element to the managed_file element that contains that button.
  1234. $parents = $form_state->getTriggeringElement()['#array_parents'];
  1235. $button_key = array_pop($parents);
  1236. $element = NestedArray::getValue($form, $parents);
  1237. // No action is needed here for the upload button, because all file uploads on
  1238. // the form are processed by \Drupal\file\Element\ManagedFile::valueCallback()
  1239. // regardless of which button was clicked. Action is needed here for the
  1240. // remove button, because we only remove a file in response to its remove
  1241. // button being clicked.
  1242. if ($button_key == 'remove_button') {
  1243. $fids = array_keys($element['#files']);
  1244. // Get files that will be removed.
  1245. if ($element['#multiple']) {
  1246. $remove_fids = [];
  1247. foreach (Element::children($element) as $name) {
  1248. if (strpos($name, 'file_') === 0 && $element[$name]['selected']['#value']) {
  1249. $remove_fids[] = (int) substr($name, 5);
  1250. }
  1251. }
  1252. $fids = array_diff($fids, $remove_fids);
  1253. }
  1254. else {
  1255. // If we deal with single upload element remove the file and set
  1256. // element's value to empty array (file could not be removed from
  1257. // element if we don't do that).
  1258. $remove_fids = $fids;
  1259. $fids = [];
  1260. }
  1261. foreach ($remove_fids as $fid) {
  1262. // If it's a temporary file we can safely remove it immediately, otherwise
  1263. // it's up to the implementing module to remove usages of files to have them
  1264. // removed.
  1265. if ($element['#files'][$fid] && $element['#files'][$fid]->isTemporary()) {
  1266. $element['#files'][$fid]->delete();
  1267. }
  1268. }
  1269. // Update both $form_state->getValues() and FormState::$input to reflect
  1270. // that the file has been removed, so that the form is rebuilt correctly.
  1271. // $form_state->getValues() must be updated in case additional submit
  1272. // handlers run, and for form building functions that run during the
  1273. // rebuild, such as when the managed_file element is part of a field widget.
  1274. // FormState::$input must be updated so that
  1275. // \Drupal\file\Element\ManagedFile::valueCallback() has correct information
  1276. // during the rebuild.
  1277. $form_state->setValueForElement($element['fids'], implode(' ', $fids));
  1278. NestedArray::setValue($form_state->getUserInput(), $element['fids']['#parents'], implode(' ', $fids));
  1279. }
  1280. // Set the form to rebuild so that $form is correctly updated in response to
  1281. // processing the file removal. Since this function did not change $form_state
  1282. // if the upload button was clicked, a rebuild isn't necessary in that
  1283. // situation and calling $form_state->disableRedirect() would suffice.
  1284. // However, we choose to always rebuild, to keep the form processing workflow
  1285. // consistent between the two buttons.
  1286. $form_state->setRebuild();
  1287. }
  1288. /**
  1289. * Saves any files that have been uploaded into a managed_file element.
  1290. *
  1291. * @param array $element
  1292. * The FAPI element whose values are being saved.
  1293. * @param \Drupal\Core\Form\FormStateInterface $form_state
  1294. * The current state of the form.
  1295. *
  1296. * @return array|false
  1297. * An array of file entities for each file that was saved, keyed by its file
  1298. * ID. Each array element contains a file entity. Function returns FALSE if
  1299. * upload directory could not be created or no files were uploaded.
  1300. */
  1301. function file_managed_file_save_upload($element, FormStateInterface $form_state) {
  1302. $upload_name = implode('_', $element['#parents']);
  1303. $all_files = \Drupal::request()->files->get('files', []);
  1304. if (empty($all_files[$upload_name])) {
  1305. return FALSE;
  1306. }
  1307. $file_upload = $all_files[$upload_name];
  1308. $destination = isset($element['#upload_location']) ? $element['#upload_location'] : NULL;
  1309. if (isset($destination) && !\Drupal::service('file_system')->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY)) {
  1310. \Drupal::logger('file')->notice('The upload directory %directory for the file field %name could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', ['%directory' => $destination, '%name' => $element['#field_name']]);
  1311. $form_state->setError($element, t('The file could not be uploaded.'));
  1312. return FALSE;
  1313. }
  1314. // Save attached files to the database.
  1315. $files_uploaded = $element['#multiple'] && count(array_filter($file_upload)) > 0;
  1316. $files_uploaded |= !$element['#multiple'] && !empty($file_upload);
  1317. if ($files_uploaded) {
  1318. if (!$files = _file_save_upload_from_form($element, $form_state)) {
  1319. \Drupal::logger('file')->notice('The file upload failed. %upload', ['%upload' => $upload_name]);
  1320. return [];
  1321. }
  1322. // Value callback expects FIDs to be keys.
  1323. $files = array_filter($files);
  1324. $fids = array_map(function ($file) {
  1325. return $file->id();
  1326. }, $files);
  1327. return empty($files) ? [] : array_combine($fids, $files);
  1328. }
  1329. return [];
  1330. }
  1331. /**
  1332. * Prepares variables for file form widget templates.
  1333. *
  1334. * Default template: file-managed-file.html.twig.
  1335. *
  1336. * @param array $variables
  1337. * An associative array containing:
  1338. * - element: A render element representing the file.
  1339. */
  1340. function template_preprocess_file_managed_file(&$variables) {
  1341. $element = $variables['element'];
  1342. $variables['attributes'] = [];
  1343. if (isset($element['#id'])) {
  1344. $variables['attributes']['id'] = $element['#id'];
  1345. }
  1346. if (!empty($element['#attributes']['class'])) {
  1347. $variables['attributes']['class'] = (array) $element['#attributes']['class'];
  1348. }
  1349. }
  1350. /**
  1351. * Prepares variables for file link templates.
  1352. *
  1353. * Default template: file-link.html.twig.
  1354. *
  1355. * @param array $variables
  1356. * An associative array containing:
  1357. * - file: A File entity to which the link will be created.
  1358. * - icon_directory: (optional) A path to a directory of icons to be used for
  1359. * files. Defaults to the value of the "icon.directory" variable.
  1360. * - description: A description to be displayed instead of the filename.
  1361. * - attributes: An associative array of attributes to be placed in the a tag.
  1362. */
  1363. function template_preprocess_file_link(&$variables) {
  1364. $file = $variables['file'];
  1365. $options = [];
  1366. // @todo Wrap in file_url_transform_relative(). This is currently
  1367. // impossible. As a work-around, we currently add the 'url.site' cache context
  1368. // to ensure different file URLs are generated for different sites in a
  1369. // multisite setup, including HTTP and HTTPS versions of the same site.
  1370. // Fix in https://www.drupal.org/node/2646744.
  1371. $url = $file->createFileUrl(FALSE);
  1372. $variables['#cache']['contexts'][] = 'url.site';
  1373. $mime_type = $file->getMimeType();
  1374. // Set options as per anchor format described at
  1375. // http://microformats.org/wiki/file-format-examples
  1376. $options['attributes']['type'] = $mime_type . '; length=' . $file->getSize();
  1377. // Use the description as the link text if available.
  1378. if (empty($variables['description'])) {
  1379. $link_text = $file->getFilename();
  1380. }
  1381. else {
  1382. $link_text = $variables['description'];
  1383. $options['attributes']['title'] = $file->getFilename();
  1384. }
  1385. // Classes to add to the file field for icons.
  1386. $classes = [
  1387. 'file',
  1388. // Add a specific class for each and every mime type.
  1389. 'file--mime-' . strtr($mime_type, ['/' => '-', '.' => '-']),
  1390. // Add a more general class for groups of well known MIME types.
  1391. 'file--' . file_icon_class($mime_type),
  1392. ];
  1393. // Set file classes to the options array.
  1394. $variables['attributes'] = new Attribute($variables['attributes']);
  1395. $variables['attributes']->addClass($classes);
  1396. $variables['file_size'] = format_size($file->getSize());
  1397. $variables['link'] = Link::fromTextAndUrl($link_text, Url::fromUri($url, $options))->toRenderable();
  1398. }
  1399. /**
  1400. * Gets a class for the icon for a MIME type.
  1401. *
  1402. * @param string $mime_type
  1403. * A MIME type.
  1404. *
  1405. * @return string
  1406. * A class associated with the file.
  1407. */
  1408. function file_icon_class($mime_type) {
  1409. // Search for a group with the files MIME type.
  1410. $generic_mime = (string) file_icon_map($mime_type);
  1411. if (!empty($generic_mime)) {
  1412. return $generic_mime;
  1413. }
  1414. // Use generic icons for each category that provides such icons.
  1415. foreach (['audio', 'image', 'text', 'video'] as $category) {
  1416. if (strpos($mime_type, $category) === 0) {
  1417. return $category;
  1418. }
  1419. }
  1420. // If there's no generic icon for the type the general class.
  1421. return 'general';
  1422. }
  1423. /**
  1424. * Determines the generic icon MIME package based on a file's MIME type.
  1425. *
  1426. * @param string $mime_type
  1427. * A MIME type.
  1428. *
  1429. * @return string|false
  1430. * The generic icon MIME package expected for this file.
  1431. */
  1432. function file_icon_map($mime_type) {
  1433. switch ($mime_type) {
  1434. // Word document types.
  1435. case 'application/msword':
  1436. case 'application/vnd.ms-word.document.macroEnabled.12':
  1437. case 'application/vnd.oasis.opendocument.text':
  1438. case 'application/vnd.oasis.opendocument.text-template':
  1439. case 'application/vnd.oasis.opendocument.text-master':
  1440. case 'application/vnd.oasis.opendocument.text-web':
  1441. case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
  1442. case 'application/vnd.stardivision.writer':
  1443. case 'application/vnd.sun.xml.writer':
  1444. case 'application/vnd.sun.xml.writer.template':
  1445. case 'application/vnd.sun.xml.writer.global':
  1446. case 'application/vnd.wordperfect':
  1447. case 'application/x-abiword':
  1448. case 'application/x-applix-word':
  1449. case 'application/x-kword':
  1450. case 'application/x-kword-crypt':
  1451. return 'x-office-document';
  1452. // Spreadsheet document types.
  1453. case 'application/vnd.ms-excel':
  1454. case 'application/vnd.ms-excel.sheet.macroEnabled.12':
  1455. case 'application/vnd.oasis.opendocument.spreadsheet':
  1456. case 'application/vnd.oasis.opendocument.spreadsheet-template':
  1457. case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
  1458. case 'application/vnd.stardivision.calc':
  1459. case 'application/vnd.sun.xml.calc':
  1460. case 'application/vnd.sun.xml.calc.template':
  1461. case 'application/vnd.lotus-1-2-3':
  1462. case 'application/x-applix-spreadsheet':
  1463. case 'application/x-gnumeric':
  1464. case 'application/x-kspread':
  1465. case 'application/x-kspread-crypt':
  1466. return 'x-office-spreadsheet';
  1467. // Presentation document types.
  1468. case 'application/vnd.ms-powerpoint':
  1469. case 'application/vnd.ms-powerpoint.presentation.macroEnabled.12':
  1470. case 'application/vnd.oasis.opendocument.presentation':
  1471. case 'application/vnd.oasis.opendocument.presentation-template':
  1472. case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
  1473. case 'application/vnd.stardivision.impress':
  1474. case 'application/vnd.sun.xml.impress':
  1475. case 'application/vnd.sun.xml.impress.template':
  1476. case 'application/x-kpresenter':
  1477. return 'x-office-presentation';
  1478. // Compressed archive types.
  1479. case 'application/zip':
  1480. case 'application/x-zip':
  1481. case 'application/stuffit':
  1482. case 'application/x-stuffit':
  1483. case 'application/x-7z-compressed':
  1484. case 'application/x-ace':
  1485. case 'application/x-arj':
  1486. case 'application/x-bzip':
  1487. case 'application/x-bzip-compressed-tar':
  1488. case 'application/x-compress':
  1489. case 'application/x-compressed-tar':
  1490. case 'application/x-cpio-compressed':
  1491. case 'application/x-deb':
  1492. case 'application/x-gzip':
  1493. case 'application/x-java-archive':
  1494. case 'application/x-lha':
  1495. case 'application/x-lhz':
  1496. case 'application/x-lzop':
  1497. case 'application/x-rar':
  1498. case 'application/x-rpm':
  1499. case 'application/x-tzo':
  1500. case 'application/x-tar':
  1501. case 'application/x-tarz':
  1502. case 'application/x-tgz':
  1503. return 'package-x-generic';
  1504. // Script file types.
  1505. case 'application/ecmascript':
  1506. case 'application/javascript':
  1507. case 'application/mathematica':
  1508. case 'application/vnd.mozilla.xul+xml':
  1509. case 'application/x-asp':
  1510. case 'application/x-awk':
  1511. case 'application/x-cgi':
  1512. case 'application/x-csh':
  1513. case 'application/x-m4':
  1514. case 'application/x-perl':
  1515. case 'application/x-php':
  1516. case 'application/x-ruby':
  1517. case 'application/x-shellscript':
  1518. case 'text/vnd.wap.wmlscript':
  1519. case 'text/x-emacs-lisp':
  1520. case 'text/x-haskell':
  1521. case 'text/x-literate-haskell':
  1522. case 'text/x-lua':
  1523. case 'text/x-makefile':
  1524. case 'text/x-matlab':
  1525. case 'text/x-python':
  1526. case 'text/x-sql':
  1527. case 'text/x-tcl':
  1528. return 'text-x-script';
  1529. // HTML aliases.
  1530. case 'application/xhtml+xml':
  1531. return 'text-html';
  1532. // Executable types.
  1533. case 'application/x-macbinary':
  1534. case 'application/x-ms-dos-executable':
  1535. case 'application/x-pef-executable':
  1536. return 'application-x-executable';
  1537. // Acrobat types
  1538. case 'application/pdf':
  1539. case 'application/x-pdf':
  1540. case 'applications/vnd.pdf':
  1541. case 'text/pdf':
  1542. case 'text/x-pdf':
  1543. return 'application-pdf';
  1544. default:
  1545. return FALSE;
  1546. }
  1547. }
  1548. /**
  1549. * Retrieves a list of references to a file.
  1550. *
  1551. * @param \Drupal\file\FileInterface $file
  1552. * A file entity.
  1553. * @param \Drupal\Core\Field\FieldDefinitionInterface|null $field
  1554. * (optional) A field definition to be used for this check. If given,
  1555. * limits the reference check to the given field. Defaults to NULL.
  1556. * @param int $age
  1557. * (optional) A constant that specifies which references to count. Use
  1558. * EntityStorageInterface::FIELD_LOAD_REVISION (the default) to retrieve all
  1559. * references within all revisions or
  1560. * EntityStorageInterface::FIELD_LOAD_CURRENT to retrieve references only in
  1561. * the current revisions of all entities that have references to this file.
  1562. * @param string $field_type
  1563. * (optional) The name of a field type. If given, limits the reference check
  1564. * to fields of the given type. If both $field and $field_type are given but
  1565. * $field is not the same type as $field_type, an empty array will be
  1566. * returned. Defaults to 'file'.
  1567. *
  1568. * @return array
  1569. * A multidimensional array. The keys are field_name, entity_type,
  1570. * entity_id and the value is an entity referencing this file.
  1571. *
  1572. * @ingroup file
  1573. */
  1574. function file_get_file_references(FileInterface $file, FieldDefinitionInterface $field = NULL, $age = EntityStorageInterface::FIELD_LOAD_REVISION, $field_type = 'file') {
  1575. $references = &drupal_static(__FUNCTION__, []);
  1576. $field_columns = &drupal_static(__FUNCTION__ . ':field_columns', []);
  1577. // Fill the static cache, disregard $field and $field_type for now.
  1578. if (!isset($references[$file->id()][$age])) {
  1579. $references[$file->id()][$age] = [];
  1580. $usage_list = \Drupal::service('file.usage')->listUsage($file);
  1581. $file_usage_list = isset($usage_list['file']) ? $usage_list['file'] : [];
  1582. foreach ($file_usage_list as $entity_type_id => $entity_ids) {
  1583. $entities = \Drupal::entityTypeManager()
  1584. ->getStorage($entity_type_id)->loadMultiple(array_keys($entity_ids));
  1585. foreach ($entities as $entity) {
  1586. $bundle = $entity->bundle();
  1587. // We need to find file fields for this entity type and bundle.
  1588. if (!isset($file_fields[$entity_type_id][$bundle])) {
  1589. $file_fields[$entity_type_id][$bundle] = [];
  1590. // This contains the possible field names.
  1591. foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) {
  1592. // If this is the first time this field type is seen, check
  1593. // whether it references files.
  1594. if (!isset($field_columns[$field_definition->getType()])) {
  1595. $field_columns[$field_definition->getType()] = file_field_find_file_reference_column($field_definition);
  1596. }
  1597. // If the field type does reference files then record it.
  1598. if ($field_columns[$field_definition->getType()]) {
  1599. $file_fields[$entity_type_id][$bundle][$field_name] = $field_columns[$field_definition->getType()];
  1600. }
  1601. }
  1602. }
  1603. foreach ($file_fields[$entity_type_id][$bundle] as $field_name => $field_column) {
  1604. // Iterate over the field items to find the referenced file and field
  1605. // name. This will fail if the usage checked is in a non-current
  1606. // revision because field items are from the current
  1607. // revision.
  1608. // We also iterate over all translations because a file can be linked
  1609. // to a language other than the default.
  1610. foreach ($entity->getTranslationLanguages() as $langcode => $language) {
  1611. foreach ($entity->getTranslation($langcode)->get($field_name) as $item) {
  1612. if ($file->id() == $item->{$field_column}) {
  1613. $references[$file->id()][$age][$field_name][$entity_type_id][$entity->id()] = $entity;
  1614. break;
  1615. }
  1616. }
  1617. }
  1618. }
  1619. }
  1620. }
  1621. }
  1622. $return = $references[$file->id()][$age];
  1623. // Filter the static cache down to the requested entries. The usual static
  1624. // cache is very small so this will be very fast.
  1625. $entity_field_manager = \Drupal::service('entity_field.manager');
  1626. if ($field || $field_type) {
  1627. foreach ($return as $field_name => $data) {
  1628. foreach (array_keys($data) as $entity_type_id) {
  1629. $field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id);
  1630. $current_field = $field_storage_definitions[$field_name];
  1631. if (($field_type && $current_field->getType() != $field_type) || ($field && $field->uuid() != $current_field->uuid())) {
  1632. unset($return[$field_name][$entity_type_id]);
  1633. }
  1634. }
  1635. }
  1636. }
  1637. return $return;
  1638. }
  1639. /**
  1640. * Formats human-readable version of file status.
  1641. *
  1642. * @param int|null $choice
  1643. * (optional) An integer status code. If not set, all statuses are returned.
  1644. * Defaults to NULL.
  1645. *
  1646. * @return \Drupal\Core\StringTranslation\TranslatableMarkup|\Drupal\Core\StringTranslation\TranslatableMarkup[]
  1647. * An array of file statuses or a specified status if $choice is set.
  1648. */
  1649. function _views_file_status($choice = NULL) {
  1650. $status = [
  1651. 0 => t('Temporary'),
  1652. FILE_STATUS_PERMANENT => t('Permanent'),
  1653. ];
  1654. if (isset($choice)) {
  1655. return isset($status[$choice]) ? $status[$choice] : t('Unknown');
  1656. }
  1657. return $status;
  1658. }