file.module 73 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814
  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. $original_file_name = trim($file_info->getClientOriginalName(), '.');
  890. // Check for file upload errors and return FALSE for this file if a lower
  891. // level system error occurred. For a complete list of errors:
  892. // See http://php.net/manual/features.file-upload.errors.php.
  893. switch ($file_info->getError()) {
  894. case UPLOAD_ERR_INI_SIZE:
  895. case UPLOAD_ERR_FORM_SIZE:
  896. \Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $original_file_name, '%maxsize' => format_size(Environment::getUploadMaxSize())]));
  897. return FALSE;
  898. case UPLOAD_ERR_PARTIAL:
  899. case UPLOAD_ERR_NO_FILE:
  900. \Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $original_file_name]));
  901. return FALSE;
  902. case UPLOAD_ERR_OK:
  903. // Final check that this is a valid upload, if it isn't, use the
  904. // default error handler.
  905. if (is_uploaded_file($file_info->getRealPath())) {
  906. break;
  907. }
  908. default:
  909. // Unknown error
  910. \Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $original_file_name]));
  911. return FALSE;
  912. }
  913. // Begin building file entity.
  914. $values = [
  915. 'uid' => $user->id(),
  916. 'status' => 0,
  917. 'filename' => $original_file_name,
  918. 'uri' => $file_info->getRealPath(),
  919. 'filesize' => $file_info->getSize(),
  920. ];
  921. $values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
  922. $file = File::create($values);
  923. $extensions = '';
  924. if (isset($validators['file_validate_extensions'])) {
  925. if (isset($validators['file_validate_extensions'][0])) {
  926. // Build the list of non-munged extensions if the caller provided them.
  927. $extensions = $validators['file_validate_extensions'][0];
  928. }
  929. else {
  930. // If 'file_validate_extensions' is set and the list is empty then the
  931. // caller wants to allow any extension. In this case we have to remove the
  932. // validator or else it will reject all extensions.
  933. unset($validators['file_validate_extensions']);
  934. }
  935. }
  936. else {
  937. // No validator was provided, so add one using the default list.
  938. // Build a default non-munged safe list for file_munge_filename().
  939. $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
  940. $validators['file_validate_extensions'] = [];
  941. $validators['file_validate_extensions'][0] = $extensions;
  942. }
  943. if (!empty($extensions)) {
  944. // Munge the filename to protect against possible malicious extension
  945. // hiding within an unknown file type (ie: filename.html.foo).
  946. $file->setFilename(file_munge_filename($file->getFilename(), $extensions));
  947. }
  948. // Rename potentially executable files, to help prevent exploits (i.e. will
  949. // rename filename.php.foo and filename.php to filename.php.foo.txt and
  950. // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
  951. // evaluates to TRUE.
  952. if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
  953. $file->setMimeType('text/plain');
  954. // The destination filename will also later be used to create the URI.
  955. $file->setFilename($file->getFilename() . '.txt');
  956. // The .txt extension may not be in the allowed list of extensions. We have
  957. // to add it here or else the file upload will fail.
  958. if (!empty($extensions)) {
  959. $validators['file_validate_extensions'][0] .= ' txt';
  960. \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
  961. }
  962. }
  963. // If the destination is not provided, use the temporary directory.
  964. if (empty($destination)) {
  965. $destination = 'temporary://';
  966. }
  967. /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
  968. $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
  969. // Assert that the destination contains a valid stream.
  970. $destination_scheme = $stream_wrapper_manager::getScheme($destination);
  971. if (!$stream_wrapper_manager->isValidScheme($destination_scheme)) {
  972. \Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
  973. return FALSE;
  974. }
  975. $file->source = $form_field_name;
  976. // A file URI may already have a trailing slash or look like "public://".
  977. if (substr($destination, -1) != '/') {
  978. $destination .= '/';
  979. }
  980. /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  981. $file_system = \Drupal::service('file_system');
  982. try {
  983. $file->destination = $file_system->getDestinationFilename($destination . $file->getFilename(), $replace);
  984. }
  985. catch (FileException $e) {
  986. \Drupal::messenger()->addError(t('The file %filename could not be uploaded because the name is invalid.', ['%filename' => $file->getFilename()]));
  987. return FALSE;
  988. }
  989. // If the destination is FALSE then there is an existing file and $replace is
  990. // set to return an error, so we need to exit.
  991. if ($file->destination === FALSE) {
  992. \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]));
  993. return FALSE;
  994. }
  995. // Add in our check of the file name length.
  996. $validators['file_validate_name_length'] = [];
  997. // Call the validation functions specified by this function's caller.
  998. $errors = file_validate($file, $validators);
  999. // Check for errors.
  1000. if (!empty($errors)) {
  1001. $message = [
  1002. 'error' => [
  1003. '#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
  1004. ],
  1005. 'item_list' => [
  1006. '#theme' => 'item_list',
  1007. '#items' => $errors,
  1008. ],
  1009. ];
  1010. // @todo Add support for render arrays in
  1011. // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
  1012. // @see https://www.drupal.org/node/2505497.
  1013. \Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
  1014. return FALSE;
  1015. }
  1016. $file->setFileUri($file->destination);
  1017. if (!$file_system->moveUploadedFile($file_info->getRealPath(), $file->getFileUri())) {
  1018. \Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
  1019. \Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
  1020. return FALSE;
  1021. }
  1022. // Set the permissions on the new file.
  1023. $file_system->chmod($file->getFileUri());
  1024. // If we are replacing an existing file re-use its database record.
  1025. // @todo Do not create a new entity in order to update it. See
  1026. // https://www.drupal.org/node/2241865.
  1027. if ($replace == FileSystemInterface::EXISTS_REPLACE) {
  1028. $existing_files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $file->getFileUri()]);
  1029. if (count($existing_files)) {
  1030. $existing = reset($existing_files);
  1031. $file->fid = $existing->id();
  1032. $file->setOriginalId($existing->id());
  1033. }
  1034. }
  1035. // Update the filename with any changes as a result of security or renaming
  1036. // due to an existing file.
  1037. $file->setFilename(\Drupal::service('file_system')->basename($file->destination));
  1038. // We can now validate the file object itself before it's saved.
  1039. $violations = $file->validate();
  1040. foreach ($violations as $violation) {
  1041. $errors[] = $violation->getMessage();
  1042. }
  1043. if (!empty($errors)) {
  1044. $message = [
  1045. 'error' => [
  1046. '#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
  1047. ],
  1048. 'item_list' => [
  1049. '#theme' => 'item_list',
  1050. '#items' => $errors,
  1051. ],
  1052. ];
  1053. // @todo Add support for render arrays in
  1054. // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
  1055. // @see https://www.drupal.org/node/2505497.
  1056. \Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
  1057. return FALSE;
  1058. }
  1059. // If we made it this far it's safe to record this file in the database.
  1060. $file->save();
  1061. // Allow an anonymous user who creates a non-public file to see it. See
  1062. // \Drupal\file\FileAccessControlHandler::checkAccess().
  1063. if ($user->isAnonymous() && $destination_scheme !== 'public') {
  1064. $session = \Drupal::request()->getSession();
  1065. $allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
  1066. $allowed_temp_files[$file->id()] = $file->id();
  1067. $session->set('anonymous_allowed_file_ids', $allowed_temp_files);
  1068. }
  1069. return $file;
  1070. }
  1071. /**
  1072. * Determines the preferred upload progress implementation.
  1073. *
  1074. * @return string|false
  1075. * A string indicating which upload progress system is available. Either "apc"
  1076. * or "uploadprogress". If neither are available, returns FALSE.
  1077. */
  1078. function file_progress_implementation() {
  1079. static $implementation;
  1080. if (!isset($implementation)) {
  1081. $implementation = FALSE;
  1082. // We prefer the PECL extension uploadprogress because it supports multiple
  1083. // simultaneous uploads. APCu only supports one at a time.
  1084. if (extension_loaded('uploadprogress')) {
  1085. $implementation = 'uploadprogress';
  1086. }
  1087. }
  1088. return $implementation;
  1089. }
  1090. /**
  1091. * Implements hook_ENTITY_TYPE_predelete() for file entities.
  1092. */
  1093. function file_file_predelete(File $file) {
  1094. // @todo Remove references to a file that is in-use.
  1095. }
  1096. /**
  1097. * Implements hook_tokens().
  1098. */
  1099. function file_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
  1100. $token_service = \Drupal::token();
  1101. $url_options = ['absolute' => TRUE];
  1102. if (isset($options['langcode'])) {
  1103. $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
  1104. $langcode = $options['langcode'];
  1105. }
  1106. else {
  1107. $langcode = NULL;
  1108. }
  1109. $replacements = [];
  1110. if ($type == 'file' && !empty($data['file'])) {
  1111. /** @var \Drupal\file\FileInterface $file */
  1112. $file = $data['file'];
  1113. foreach ($tokens as $name => $original) {
  1114. switch ($name) {
  1115. // Basic keys and values.
  1116. case 'fid':
  1117. $replacements[$original] = $file->id();
  1118. break;
  1119. // Essential file data
  1120. case 'name':
  1121. $replacements[$original] = $file->getFilename();
  1122. break;
  1123. case 'path':
  1124. $replacements[$original] = $file->getFileUri();
  1125. break;
  1126. case 'mime':
  1127. $replacements[$original] = $file->getMimeType();
  1128. break;
  1129. case 'size':
  1130. $replacements[$original] = format_size($file->getSize());
  1131. break;
  1132. case 'url':
  1133. // Ideally, this would use file_url_transform_relative(), but because
  1134. // tokens are also often used in e-mails, it's better to keep absolute
  1135. // file URLs. The 'url.site' cache context is associated to ensure the
  1136. // correct absolute URL is used in case of a multisite setup.
  1137. $replacements[$original] = $file->createFileUrl(FALSE);
  1138. $bubbleable_metadata->addCacheContexts(['url.site']);
  1139. break;
  1140. // These tokens are default variations on the chained tokens handled below.
  1141. case 'created':
  1142. $date_format = DateFormat::load('medium');
  1143. $bubbleable_metadata->addCacheableDependency($date_format);
  1144. $replacements[$original] = \Drupal::service('date.formatter')->format($file->getCreatedTime(), 'medium', '', NULL, $langcode);
  1145. break;
  1146. case 'changed':
  1147. $date_format = DateFormat::load('medium');
  1148. $bubbleable_metadata = $bubbleable_metadata->addCacheableDependency($date_format);
  1149. $replacements[$original] = \Drupal::service('date.formatter')->format($file->getChangedTime(), 'medium', '', NULL, $langcode);
  1150. break;
  1151. case 'owner':
  1152. $owner = $file->getOwner();
  1153. $bubbleable_metadata->addCacheableDependency($owner);
  1154. $name = $owner->label();
  1155. $replacements[$original] = $name;
  1156. break;
  1157. }
  1158. }
  1159. if ($date_tokens = $token_service->findWithPrefix($tokens, 'created')) {
  1160. $replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getCreatedTime()], $options, $bubbleable_metadata);
  1161. }
  1162. if ($date_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
  1163. $replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getChangedTime()], $options, $bubbleable_metadata);
  1164. }
  1165. if (($owner_tokens = $token_service->findWithPrefix($tokens, 'owner')) && $file->getOwner()) {
  1166. $replacements += $token_service->generate('user', $owner_tokens, ['user' => $file->getOwner()], $options, $bubbleable_metadata);
  1167. }
  1168. }
  1169. return $replacements;
  1170. }
  1171. /**
  1172. * Implements hook_token_info().
  1173. */
  1174. function file_token_info() {
  1175. $types['file'] = [
  1176. 'name' => t("Files"),
  1177. 'description' => t("Tokens related to uploaded files."),
  1178. 'needs-data' => 'file',
  1179. ];
  1180. // File related tokens.
  1181. $file['fid'] = [
  1182. 'name' => t("File ID"),
  1183. 'description' => t("The unique ID of the uploaded file."),
  1184. ];
  1185. $file['name'] = [
  1186. 'name' => t("File name"),
  1187. 'description' => t("The name of the file on disk."),
  1188. ];
  1189. $file['path'] = [
  1190. 'name' => t("Path"),
  1191. 'description' => t("The location of the file relative to Drupal root."),
  1192. ];
  1193. $file['mime'] = [
  1194. 'name' => t("MIME type"),
  1195. 'description' => t("The MIME type of the file."),
  1196. ];
  1197. $file['size'] = [
  1198. 'name' => t("File size"),
  1199. 'description' => t("The size of the file."),
  1200. ];
  1201. $file['url'] = [
  1202. 'name' => t("URL"),
  1203. 'description' => t("The web-accessible URL for the file."),
  1204. ];
  1205. $file['created'] = [
  1206. 'name' => t("Created"),
  1207. 'description' => t("The date the file created."),
  1208. 'type' => 'date',
  1209. ];
  1210. $file['changed'] = [
  1211. 'name' => t("Changed"),
  1212. 'description' => t("The date the file was most recently changed."),
  1213. 'type' => 'date',
  1214. ];
  1215. $file['owner'] = [
  1216. 'name' => t("Owner"),
  1217. 'description' => t("The user who originally uploaded the file."),
  1218. 'type' => 'user',
  1219. ];
  1220. return [
  1221. 'types' => $types,
  1222. 'tokens' => [
  1223. 'file' => $file,
  1224. ],
  1225. ];
  1226. }
  1227. /**
  1228. * Form submission handler for upload / remove buttons of managed_file elements.
  1229. *
  1230. * @see \Drupal\file\Element\ManagedFile::processManagedFile()
  1231. */
  1232. function file_managed_file_submit($form, FormStateInterface $form_state) {
  1233. // Determine whether it was the upload or the remove button that was clicked,
  1234. // and set $element to the managed_file element that contains that button.
  1235. $parents = $form_state->getTriggeringElement()['#array_parents'];
  1236. $button_key = array_pop($parents);
  1237. $element = NestedArray::getValue($form, $parents);
  1238. // No action is needed here for the upload button, because all file uploads on
  1239. // the form are processed by \Drupal\file\Element\ManagedFile::valueCallback()
  1240. // regardless of which button was clicked. Action is needed here for the
  1241. // remove button, because we only remove a file in response to its remove
  1242. // button being clicked.
  1243. if ($button_key == 'remove_button') {
  1244. $fids = array_keys($element['#files']);
  1245. // Get files that will be removed.
  1246. if ($element['#multiple']) {
  1247. $remove_fids = [];
  1248. foreach (Element::children($element) as $name) {
  1249. if (strpos($name, 'file_') === 0 && $element[$name]['selected']['#value']) {
  1250. $remove_fids[] = (int) substr($name, 5);
  1251. }
  1252. }
  1253. $fids = array_diff($fids, $remove_fids);
  1254. }
  1255. else {
  1256. // If we deal with single upload element remove the file and set
  1257. // element's value to empty array (file could not be removed from
  1258. // element if we don't do that).
  1259. $remove_fids = $fids;
  1260. $fids = [];
  1261. }
  1262. foreach ($remove_fids as $fid) {
  1263. // If it's a temporary file we can safely remove it immediately, otherwise
  1264. // it's up to the implementing module to remove usages of files to have them
  1265. // removed.
  1266. if ($element['#files'][$fid] && $element['#files'][$fid]->isTemporary()) {
  1267. $element['#files'][$fid]->delete();
  1268. }
  1269. }
  1270. // Update both $form_state->getValues() and FormState::$input to reflect
  1271. // that the file has been removed, so that the form is rebuilt correctly.
  1272. // $form_state->getValues() must be updated in case additional submit
  1273. // handlers run, and for form building functions that run during the
  1274. // rebuild, such as when the managed_file element is part of a field widget.
  1275. // FormState::$input must be updated so that
  1276. // \Drupal\file\Element\ManagedFile::valueCallback() has correct information
  1277. // during the rebuild.
  1278. $form_state->setValueForElement($element['fids'], implode(' ', $fids));
  1279. NestedArray::setValue($form_state->getUserInput(), $element['fids']['#parents'], implode(' ', $fids));
  1280. }
  1281. // Set the form to rebuild so that $form is correctly updated in response to
  1282. // processing the file removal. Since this function did not change $form_state
  1283. // if the upload button was clicked, a rebuild isn't necessary in that
  1284. // situation and calling $form_state->disableRedirect() would suffice.
  1285. // However, we choose to always rebuild, to keep the form processing workflow
  1286. // consistent between the two buttons.
  1287. $form_state->setRebuild();
  1288. }
  1289. /**
  1290. * Saves any files that have been uploaded into a managed_file element.
  1291. *
  1292. * @param array $element
  1293. * The FAPI element whose values are being saved.
  1294. * @param \Drupal\Core\Form\FormStateInterface $form_state
  1295. * The current state of the form.
  1296. *
  1297. * @return array|false
  1298. * An array of file entities for each file that was saved, keyed by its file
  1299. * ID. Each array element contains a file entity. Function returns FALSE if
  1300. * upload directory could not be created or no files were uploaded.
  1301. */
  1302. function file_managed_file_save_upload($element, FormStateInterface $form_state) {
  1303. $upload_name = implode('_', $element['#parents']);
  1304. $all_files = \Drupal::request()->files->get('files', []);
  1305. if (empty($all_files[$upload_name])) {
  1306. return FALSE;
  1307. }
  1308. $file_upload = $all_files[$upload_name];
  1309. $destination = isset($element['#upload_location']) ? $element['#upload_location'] : NULL;
  1310. if (isset($destination) && !\Drupal::service('file_system')->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY)) {
  1311. \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']]);
  1312. $form_state->setError($element, t('The file could not be uploaded.'));
  1313. return FALSE;
  1314. }
  1315. // Save attached files to the database.
  1316. $files_uploaded = $element['#multiple'] && count(array_filter($file_upload)) > 0;
  1317. $files_uploaded |= !$element['#multiple'] && !empty($file_upload);
  1318. if ($files_uploaded) {
  1319. if (!$files = _file_save_upload_from_form($element, $form_state)) {
  1320. \Drupal::logger('file')->notice('The file upload failed. %upload', ['%upload' => $upload_name]);
  1321. return [];
  1322. }
  1323. // Value callback expects FIDs to be keys.
  1324. $files = array_filter($files);
  1325. $fids = array_map(function ($file) {
  1326. return $file->id();
  1327. }, $files);
  1328. return empty($files) ? [] : array_combine($fids, $files);
  1329. }
  1330. return [];
  1331. }
  1332. /**
  1333. * Prepares variables for file form widget templates.
  1334. *
  1335. * Default template: file-managed-file.html.twig.
  1336. *
  1337. * @param array $variables
  1338. * An associative array containing:
  1339. * - element: A render element representing the file.
  1340. */
  1341. function template_preprocess_file_managed_file(&$variables) {
  1342. $element = $variables['element'];
  1343. $variables['attributes'] = [];
  1344. if (isset($element['#id'])) {
  1345. $variables['attributes']['id'] = $element['#id'];
  1346. }
  1347. if (!empty($element['#attributes']['class'])) {
  1348. $variables['attributes']['class'] = (array) $element['#attributes']['class'];
  1349. }
  1350. }
  1351. /**
  1352. * Prepares variables for file link templates.
  1353. *
  1354. * Default template: file-link.html.twig.
  1355. *
  1356. * @param array $variables
  1357. * An associative array containing:
  1358. * - file: A File entity to which the link will be created.
  1359. * - icon_directory: (optional) A path to a directory of icons to be used for
  1360. * files. Defaults to the value of the "icon.directory" variable.
  1361. * - description: A description to be displayed instead of the filename.
  1362. * - attributes: An associative array of attributes to be placed in the a tag.
  1363. */
  1364. function template_preprocess_file_link(&$variables) {
  1365. $file = $variables['file'];
  1366. $options = [];
  1367. // @todo Wrap in file_url_transform_relative(). This is currently
  1368. // impossible. As a work-around, we currently add the 'url.site' cache context
  1369. // to ensure different file URLs are generated for different sites in a
  1370. // multisite setup, including HTTP and HTTPS versions of the same site.
  1371. // Fix in https://www.drupal.org/node/2646744.
  1372. $url = $file->createFileUrl(FALSE);
  1373. $variables['#cache']['contexts'][] = 'url.site';
  1374. $mime_type = $file->getMimeType();
  1375. // Set options as per anchor format described at
  1376. // http://microformats.org/wiki/file-format-examples
  1377. $options['attributes']['type'] = $mime_type . '; length=' . $file->getSize();
  1378. // Use the description as the link text if available.
  1379. if (empty($variables['description'])) {
  1380. $link_text = $file->getFilename();
  1381. }
  1382. else {
  1383. $link_text = $variables['description'];
  1384. $options['attributes']['title'] = $file->getFilename();
  1385. }
  1386. // Classes to add to the file field for icons.
  1387. $classes = [
  1388. 'file',
  1389. // Add a specific class for each and every mime type.
  1390. 'file--mime-' . strtr($mime_type, ['/' => '-', '.' => '-']),
  1391. // Add a more general class for groups of well known MIME types.
  1392. 'file--' . file_icon_class($mime_type),
  1393. ];
  1394. // Set file classes to the options array.
  1395. $variables['attributes'] = new Attribute($variables['attributes']);
  1396. $variables['attributes']->addClass($classes);
  1397. $variables['file_size'] = format_size($file->getSize());
  1398. $variables['link'] = Link::fromTextAndUrl($link_text, Url::fromUri($url, $options))->toRenderable();
  1399. }
  1400. /**
  1401. * Gets a class for the icon for a MIME type.
  1402. *
  1403. * @param string $mime_type
  1404. * A MIME type.
  1405. *
  1406. * @return string
  1407. * A class associated with the file.
  1408. */
  1409. function file_icon_class($mime_type) {
  1410. // Search for a group with the files MIME type.
  1411. $generic_mime = (string) file_icon_map($mime_type);
  1412. if (!empty($generic_mime)) {
  1413. return $generic_mime;
  1414. }
  1415. // Use generic icons for each category that provides such icons.
  1416. foreach (['audio', 'image', 'text', 'video'] as $category) {
  1417. if (strpos($mime_type, $category) === 0) {
  1418. return $category;
  1419. }
  1420. }
  1421. // If there's no generic icon for the type the general class.
  1422. return 'general';
  1423. }
  1424. /**
  1425. * Determines the generic icon MIME package based on a file's MIME type.
  1426. *
  1427. * @param string $mime_type
  1428. * A MIME type.
  1429. *
  1430. * @return string|false
  1431. * The generic icon MIME package expected for this file.
  1432. */
  1433. function file_icon_map($mime_type) {
  1434. switch ($mime_type) {
  1435. // Word document types.
  1436. case 'application/msword':
  1437. case 'application/vnd.ms-word.document.macroEnabled.12':
  1438. case 'application/vnd.oasis.opendocument.text':
  1439. case 'application/vnd.oasis.opendocument.text-template':
  1440. case 'application/vnd.oasis.opendocument.text-master':
  1441. case 'application/vnd.oasis.opendocument.text-web':
  1442. case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
  1443. case 'application/vnd.stardivision.writer':
  1444. case 'application/vnd.sun.xml.writer':
  1445. case 'application/vnd.sun.xml.writer.template':
  1446. case 'application/vnd.sun.xml.writer.global':
  1447. case 'application/vnd.wordperfect':
  1448. case 'application/x-abiword':
  1449. case 'application/x-applix-word':
  1450. case 'application/x-kword':
  1451. case 'application/x-kword-crypt':
  1452. return 'x-office-document';
  1453. // Spreadsheet document types.
  1454. case 'application/vnd.ms-excel':
  1455. case 'application/vnd.ms-excel.sheet.macroEnabled.12':
  1456. case 'application/vnd.oasis.opendocument.spreadsheet':
  1457. case 'application/vnd.oasis.opendocument.spreadsheet-template':
  1458. case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
  1459. case 'application/vnd.stardivision.calc':
  1460. case 'application/vnd.sun.xml.calc':
  1461. case 'application/vnd.sun.xml.calc.template':
  1462. case 'application/vnd.lotus-1-2-3':
  1463. case 'application/x-applix-spreadsheet':
  1464. case 'application/x-gnumeric':
  1465. case 'application/x-kspread':
  1466. case 'application/x-kspread-crypt':
  1467. return 'x-office-spreadsheet';
  1468. // Presentation document types.
  1469. case 'application/vnd.ms-powerpoint':
  1470. case 'application/vnd.ms-powerpoint.presentation.macroEnabled.12':
  1471. case 'application/vnd.oasis.opendocument.presentation':
  1472. case 'application/vnd.oasis.opendocument.presentation-template':
  1473. case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
  1474. case 'application/vnd.stardivision.impress':
  1475. case 'application/vnd.sun.xml.impress':
  1476. case 'application/vnd.sun.xml.impress.template':
  1477. case 'application/x-kpresenter':
  1478. return 'x-office-presentation';
  1479. // Compressed archive types.
  1480. case 'application/zip':
  1481. case 'application/x-zip':
  1482. case 'application/stuffit':
  1483. case 'application/x-stuffit':
  1484. case 'application/x-7z-compressed':
  1485. case 'application/x-ace':
  1486. case 'application/x-arj':
  1487. case 'application/x-bzip':
  1488. case 'application/x-bzip-compressed-tar':
  1489. case 'application/x-compress':
  1490. case 'application/x-compressed-tar':
  1491. case 'application/x-cpio-compressed':
  1492. case 'application/x-deb':
  1493. case 'application/x-gzip':
  1494. case 'application/x-java-archive':
  1495. case 'application/x-lha':
  1496. case 'application/x-lhz':
  1497. case 'application/x-lzop':
  1498. case 'application/x-rar':
  1499. case 'application/x-rpm':
  1500. case 'application/x-tzo':
  1501. case 'application/x-tar':
  1502. case 'application/x-tarz':
  1503. case 'application/x-tgz':
  1504. return 'package-x-generic';
  1505. // Script file types.
  1506. case 'application/ecmascript':
  1507. case 'application/javascript':
  1508. case 'application/mathematica':
  1509. case 'application/vnd.mozilla.xul+xml':
  1510. case 'application/x-asp':
  1511. case 'application/x-awk':
  1512. case 'application/x-cgi':
  1513. case 'application/x-csh':
  1514. case 'application/x-m4':
  1515. case 'application/x-perl':
  1516. case 'application/x-php':
  1517. case 'application/x-ruby':
  1518. case 'application/x-shellscript':
  1519. case 'text/vnd.wap.wmlscript':
  1520. case 'text/x-emacs-lisp':
  1521. case 'text/x-haskell':
  1522. case 'text/x-literate-haskell':
  1523. case 'text/x-lua':
  1524. case 'text/x-makefile':
  1525. case 'text/x-matlab':
  1526. case 'text/x-python':
  1527. case 'text/x-sql':
  1528. case 'text/x-tcl':
  1529. return 'text-x-script';
  1530. // HTML aliases.
  1531. case 'application/xhtml+xml':
  1532. return 'text-html';
  1533. // Executable types.
  1534. case 'application/x-macbinary':
  1535. case 'application/x-ms-dos-executable':
  1536. case 'application/x-pef-executable':
  1537. return 'application-x-executable';
  1538. // Acrobat types
  1539. case 'application/pdf':
  1540. case 'application/x-pdf':
  1541. case 'applications/vnd.pdf':
  1542. case 'text/pdf':
  1543. case 'text/x-pdf':
  1544. return 'application-pdf';
  1545. default:
  1546. return FALSE;
  1547. }
  1548. }
  1549. /**
  1550. * Retrieves a list of references to a file.
  1551. *
  1552. * @param \Drupal\file\FileInterface $file
  1553. * A file entity.
  1554. * @param \Drupal\Core\Field\FieldDefinitionInterface|null $field
  1555. * (optional) A field definition to be used for this check. If given,
  1556. * limits the reference check to the given field. Defaults to NULL.
  1557. * @param int $age
  1558. * (optional) A constant that specifies which references to count. Use
  1559. * EntityStorageInterface::FIELD_LOAD_REVISION (the default) to retrieve all
  1560. * references within all revisions or
  1561. * EntityStorageInterface::FIELD_LOAD_CURRENT to retrieve references only in
  1562. * the current revisions of all entities that have references to this file.
  1563. * @param string $field_type
  1564. * (optional) The name of a field type. If given, limits the reference check
  1565. * to fields of the given type. If both $field and $field_type are given but
  1566. * $field is not the same type as $field_type, an empty array will be
  1567. * returned. Defaults to 'file'.
  1568. *
  1569. * @return array
  1570. * A multidimensional array. The keys are field_name, entity_type,
  1571. * entity_id and the value is an entity referencing this file.
  1572. *
  1573. * @ingroup file
  1574. */
  1575. function file_get_file_references(FileInterface $file, FieldDefinitionInterface $field = NULL, $age = EntityStorageInterface::FIELD_LOAD_REVISION, $field_type = 'file') {
  1576. $references = &drupal_static(__FUNCTION__, []);
  1577. $field_columns = &drupal_static(__FUNCTION__ . ':field_columns', []);
  1578. // Fill the static cache, disregard $field and $field_type for now.
  1579. if (!isset($references[$file->id()][$age])) {
  1580. $references[$file->id()][$age] = [];
  1581. $usage_list = \Drupal::service('file.usage')->listUsage($file);
  1582. $file_usage_list = isset($usage_list['file']) ? $usage_list['file'] : [];
  1583. foreach ($file_usage_list as $entity_type_id => $entity_ids) {
  1584. $entities = \Drupal::entityTypeManager()
  1585. ->getStorage($entity_type_id)->loadMultiple(array_keys($entity_ids));
  1586. foreach ($entities as $entity) {
  1587. $bundle = $entity->bundle();
  1588. // We need to find file fields for this entity type and bundle.
  1589. if (!isset($file_fields[$entity_type_id][$bundle])) {
  1590. $file_fields[$entity_type_id][$bundle] = [];
  1591. // This contains the possible field names.
  1592. foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) {
  1593. // If this is the first time this field type is seen, check
  1594. // whether it references files.
  1595. if (!isset($field_columns[$field_definition->getType()])) {
  1596. $field_columns[$field_definition->getType()] = file_field_find_file_reference_column($field_definition);
  1597. }
  1598. // If the field type does reference files then record it.
  1599. if ($field_columns[$field_definition->getType()]) {
  1600. $file_fields[$entity_type_id][$bundle][$field_name] = $field_columns[$field_definition->getType()];
  1601. }
  1602. }
  1603. }
  1604. foreach ($file_fields[$entity_type_id][$bundle] as $field_name => $field_column) {
  1605. // Iterate over the field items to find the referenced file and field
  1606. // name. This will fail if the usage checked is in a non-current
  1607. // revision because field items are from the current
  1608. // revision.
  1609. // We also iterate over all translations because a file can be linked
  1610. // to a language other than the default.
  1611. foreach ($entity->getTranslationLanguages() as $langcode => $language) {
  1612. foreach ($entity->getTranslation($langcode)->get($field_name) as $item) {
  1613. if ($file->id() == $item->{$field_column}) {
  1614. $references[$file->id()][$age][$field_name][$entity_type_id][$entity->id()] = $entity;
  1615. break;
  1616. }
  1617. }
  1618. }
  1619. }
  1620. }
  1621. }
  1622. }
  1623. $return = $references[$file->id()][$age];
  1624. // Filter the static cache down to the requested entries. The usual static
  1625. // cache is very small so this will be very fast.
  1626. $entity_field_manager = \Drupal::service('entity_field.manager');
  1627. if ($field || $field_type) {
  1628. foreach ($return as $field_name => $data) {
  1629. foreach (array_keys($data) as $entity_type_id) {
  1630. $field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id);
  1631. $current_field = $field_storage_definitions[$field_name];
  1632. if (($field_type && $current_field->getType() != $field_type) || ($field && $field->uuid() != $current_field->uuid())) {
  1633. unset($return[$field_name][$entity_type_id]);
  1634. }
  1635. }
  1636. }
  1637. }
  1638. return $return;
  1639. }
  1640. /**
  1641. * Formats human-readable version of file status.
  1642. *
  1643. * @param int|null $choice
  1644. * (optional) An integer status code. If not set, all statuses are returned.
  1645. * Defaults to NULL.
  1646. *
  1647. * @return \Drupal\Core\StringTranslation\TranslatableMarkup|\Drupal\Core\StringTranslation\TranslatableMarkup[]
  1648. * An array of file statuses or a specified status if $choice is set.
  1649. */
  1650. function _views_file_status($choice = NULL) {
  1651. $status = [
  1652. 0 => t('Temporary'),
  1653. FILE_STATUS_PERMANENT => t('Permanent'),
  1654. ];
  1655. if (isset($choice)) {
  1656. return isset($status[$choice]) ? $status[$choice] : t('Unknown');
  1657. }
  1658. return $status;
  1659. }