file_entity.module 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. <?php
  2. /**
  3. * @file
  4. * Extends Drupal file entities to be fieldable and viewable.
  5. */
  6. /**
  7. * As part of extending Drupal core's file entity API, this module adds some
  8. * functions to the 'file' namespace. For organization, those are kept in the
  9. * 'file_entity.file_api.inc' file.
  10. */
  11. require_once dirname(__FILE__) . '/file_entity.file_api.inc';
  12. /**
  13. * Implements hook_help().
  14. */
  15. function file_entity_help($path, $arg) {
  16. switch ($path) {
  17. case 'admin/config/media/file-types':
  18. $output = '<p>' . t('When a file is uploaded to this website, it is assigned one of the following types, based on what kind of file it is.') . '</p>';
  19. return $output;
  20. }
  21. }
  22. /**
  23. * Implements hook_menu().
  24. */
  25. function file_entity_menu() {
  26. $items['admin/config/media/file-types'] = array(
  27. 'title' => 'File types',
  28. 'description' => 'Manage files used on your site.',
  29. 'page callback' => 'file_entity_list_types_page',
  30. 'access arguments' => array('administer site configuration'),
  31. 'file' => 'file_entity.admin.inc',
  32. );
  33. $items['admin/config/media/file-types/manage/%'] = array(
  34. 'title' => 'Manage file types',
  35. 'description' => 'Manage files used on your site.',
  36. );
  37. // Attach a "Manage file display" tab to each file type in the same way that
  38. // Field UI attaches "Manage fields" and "Manage display" tabs. Note that
  39. // Field UI does not have to be enabled; we're just using the same IA pattern
  40. // here for attaching the "Manage file display" page.
  41. $entity_info = entity_get_info('file');
  42. foreach ($entity_info['bundles'] as $file_type => $bundle_info) {
  43. if (isset($bundle_info['admin'])) {
  44. // Get the base path and access.
  45. $path = $bundle_info['admin']['path'];
  46. $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments')));
  47. $access += array(
  48. 'access callback' => 'user_access',
  49. 'access arguments' => array('administer site configuration'),
  50. );
  51. // The file type must be passed to the page callbacks. It might be
  52. // configured as a wildcard (multiple file types sharing the same menu
  53. // router path).
  54. $file_type_argument = isset($bundle_info['admin']['bundle argument']) ? $bundle_info['admin']['bundle argument'] : $file_type;
  55. // Add the 'Manage file display' tab.
  56. $items["$path/file-display"] = array(
  57. 'title' => 'Manage file display',
  58. 'page callback' => 'drupal_get_form',
  59. 'page arguments' => array('file_entity_file_display_form', $file_type_argument, 'default'),
  60. 'type' => MENU_LOCAL_TASK,
  61. 'weight' => 3,
  62. 'file' => 'file_entity.admin.inc',
  63. ) + $access;
  64. // Add a secondary tab for each view mode.
  65. $weight = 0;
  66. $view_modes = array('default' => array('label' => t('Default'))) + $entity_info['view modes'];
  67. foreach ($view_modes as $view_mode => $view_mode_info) {
  68. $items["$path/file-display/$view_mode"] = array(
  69. 'title' => $view_mode_info['label'],
  70. 'page arguments' => array('file_entity_file_display_form', $file_type_argument, $view_mode),
  71. 'type' => ($view_mode == 'default' ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK),
  72. 'weight' => ($view_mode == 'default' ? -10 : $weight++),
  73. 'file' => 'file_entity.admin.inc',
  74. // View modes for which the 'custom settings' flag isn't TRUE are
  75. // disabled via this access callback. This needs to extend, rather
  76. // than override normal $access rules.
  77. 'access callback' => '_file_entity_view_mode_menu_access',
  78. 'access arguments' => array_merge(array($file_type_argument, $view_mode, $access['access callback']), $access['access arguments']),
  79. );
  80. }
  81. }
  82. }
  83. return $items;
  84. }
  85. /**
  86. * Implements hook_theme().
  87. */
  88. function file_entity_theme() {
  89. return array(
  90. 'file_entity_file_type_overview' => array(
  91. 'variables' => array('label' => NULL, 'description' => NULL),
  92. 'file' => 'file_entity.admin.inc',
  93. ),
  94. 'file_entity_file_display_order' => array(
  95. 'render element' => 'element',
  96. 'file' => 'file_entity.admin.inc',
  97. ),
  98. );
  99. }
  100. /**
  101. * Implements hook_entity_info_alter().
  102. *
  103. * Extends the core file entity to be fieldable. Modules can define file types
  104. * via hook_file_type_info(). For each defined type, create a bundle, so that
  105. * fields can be configured per file type.
  106. */
  107. function file_entity_entity_info_alter(&$entity_info) {
  108. $entity_info['file']['fieldable'] = TRUE;
  109. $entity_info['file']['entity keys']['bundle'] = 'type';
  110. $entity_info['file']['bundle keys']['bundle'] = 'type';
  111. $entity_info['file']['bundles'] = array();
  112. foreach (file_info_file_types() as $type => $info) {
  113. $info += array(
  114. // Provide a default administration path for Field UI, but not if 'admin'
  115. // has been explicitly set to NULL.
  116. 'admin' => array(
  117. 'path' => 'admin/config/media/file-types/manage/%file_type',
  118. 'real path' => 'admin/config/media/file-types/manage/' . $type,
  119. 'bundle argument' => 5,
  120. ),
  121. );
  122. $entity_info['file']['bundles'][$type] = array_intersect_key($info, drupal_map_assoc(array('label', 'admin')));
  123. }
  124. }
  125. /**
  126. * Implements hook_field_extra_fields().
  127. *
  128. * Adds 'file' as an extra field, so that its display and form component can be
  129. * weighted relative to the fields that are added to file entity bundles.
  130. */
  131. function file_entity_field_extra_fields() {
  132. $return = array();
  133. $info = entity_get_info('file');
  134. foreach (array_keys($info['bundles']) as $bundle) {
  135. $return['file'][$bundle] = array(
  136. 'form' => array(
  137. 'file' => array(
  138. 'label' => t('File'),
  139. 'description' => t('File preview'),
  140. 'weight' => 0,
  141. ),
  142. ),
  143. 'display' => array(
  144. 'file' => array(
  145. 'label' => t('File'),
  146. 'description' => t('File display'),
  147. 'weight' => 0,
  148. ),
  149. ),
  150. );
  151. }
  152. return $return;
  153. }
  154. /**
  155. * Implements hook_file_presave().
  156. */
  157. function file_entity_file_presave($file) {
  158. // The file type is a bundle key, so can't be NULL. file_entity_schema_alter()
  159. // ensures that it isn't NULL after a file_load(). However, file_save() can be
  160. // called on a new file object, so we apply the default here as well.
  161. if (!isset($file->type)) {
  162. $file->type = FILE_TYPE_NONE;
  163. }
  164. // If the file doesn't already have a real type, attempt to assign it one.
  165. if ($file->type == FILE_TYPE_NONE && ($type = file_get_type($file))) {
  166. $file->type = $type;
  167. }
  168. field_attach_presave('file', $file);
  169. }
  170. /**
  171. * Implements hook_file_insert().
  172. */
  173. function file_entity_file_insert($file) {
  174. field_attach_insert('file', $file);
  175. }
  176. /**
  177. * Implement hook_file_update().
  178. */
  179. function file_entity_file_update($file) {
  180. field_attach_update('file', $file);
  181. }
  182. /**
  183. * Implements hook_file_delete().
  184. */
  185. function file_entity_file_delete($file) {
  186. field_attach_delete('file', $file);
  187. }
  188. /**
  189. * Implements hook_file_formatter_info().
  190. */
  191. function file_entity_file_formatter_info() {
  192. $formatters = array();
  193. // Allow file field formatters to be reused for displaying the file entity's
  194. // file pseudo-field.
  195. if (module_exists('file')) {
  196. foreach (field_info_formatter_types() as $field_formatter_type => $field_formatter_info) {
  197. if (in_array('file', $field_formatter_info['field types'])) {
  198. $formatters['file_field_' . $field_formatter_type] = array(
  199. 'label' => $field_formatter_info['label'],
  200. 'view callback' => 'file_entity_file_formatter_file_field_view',
  201. );
  202. if (isset($field_formatter_info['settings'])) {
  203. $formatters['file_field_' . $field_formatter_type] += array(
  204. 'default settings' => $field_formatter_info['settings'],
  205. 'settings callback' => 'file_entity_file_formatter_file_field_settings',
  206. );
  207. }
  208. }
  209. }
  210. }
  211. // Add a simple file formatter for displaying an image in a chosen style.
  212. if (module_exists('image')) {
  213. $formatters['file_image'] = array(
  214. 'label' => t('Image'),
  215. 'default settings' => array('image_style' => ''),
  216. 'view callback' => 'file_entity_file_formatter_file_image_view',
  217. 'settings callback' => 'file_entity_file_formatter_file_image_settings',
  218. );
  219. }
  220. return $formatters;
  221. }
  222. /**
  223. * Implements hook_file_formatter_FORMATTER_view().
  224. *
  225. * This function provides a bridge to the field formatter API, so that file
  226. * field formatters can be reused for displaying the file entity's file
  227. * pseudo-field.
  228. */
  229. function file_entity_file_formatter_file_field_view($file, $display, $langcode) {
  230. if (strpos($display['type'], 'file_field_') === 0) {
  231. $field_formatter_type = substr($display['type'], strlen('file_field_'));
  232. $field_formatter_info = field_info_formatter_types($field_formatter_type);
  233. if (isset($field_formatter_info['module'])) {
  234. // Set $display['type'] to what hook_field_formatter_*() expects.
  235. $display['type'] = $field_formatter_type;
  236. // Set $items to what file field formatters expect. See file_field_load(),
  237. // and note that, here, $file is already a fully loaded entity.
  238. $items = array((array) $file);
  239. // Invoke hook_field_formatter_prepare_view() and
  240. // hook_field_formatter_view(). Note that we are reusing field formatter
  241. // functions, but we are not displaying a Field API field, so we set
  242. // $field and $instance accordingly, and do not invoke
  243. // hook_field_prepare_view(). This assumes that the formatter functions do
  244. // not rely on $field or $instance. A module that implements formatter
  245. // functions that rely on $field or $instance (and therefore, can only be
  246. // used for real fields) can prevent this formatter from being used on the
  247. // pseudo-field by removing it within hook_file_formatter_info_alter().
  248. $field = $instance = NULL;
  249. if (($function = ($field_formatter_info['module'] . '_field_formatter_prepare_view')) && function_exists($function)) {
  250. $fid = $file->fid;
  251. // hook_field_formatter_prepare_view() alters $items by reference.
  252. $grouped_items = array($fid => &$items);
  253. $function('file', array($fid => $file), $field, array($fid => $instance), $langcode, $grouped_items, array($fid => $display));
  254. }
  255. if (($function = ($field_formatter_info['module'] . '_field_formatter_view')) && function_exists($function)) {
  256. $element = $function('file', $file, $field, $instance, $langcode, $items, $display);
  257. // We passed the file as $items[0], so return the corresponding element.
  258. if (isset($element[0])) {
  259. return $element[0];
  260. }
  261. }
  262. }
  263. }
  264. }
  265. /**
  266. * Implements hook_file_formatter_FORMATTER_settings().
  267. *
  268. * This function provides a bridge to the field formatter API, so that file
  269. * field formatters can be reused for displaying the file entity's file
  270. * pseudo-field.
  271. */
  272. function file_entity_file_formatter_file_field_settings($form, &$form_state, $settings, $formatter_type, $file_type, $view_mode) {
  273. if (strpos($formatter_type, 'file_field_') === 0) {
  274. $field_formatter_type = substr($formatter_type, strlen('file_field_'));
  275. $field_formatter_info = field_info_formatter_types($field_formatter_type);
  276. // Invoke hook_field_formatter_settings_form(). We are reusing field
  277. // formatter functions, but we are not working with a Field API field, so
  278. // set $field accordingly. Unfortunately, the API is for $settings to be
  279. // transfered via the $instance parameter, so we must mock it.
  280. if (isset($field_formatter_info['module']) && ($function = ($field_formatter_info['module'] . '_field_formatter_settings_form')) && function_exists($function)) {
  281. $field = NULL;
  282. $mock_instance['display'][$view_mode] = array(
  283. 'type' => $field_formatter_type,
  284. 'settings' => $settings,
  285. );
  286. return $function($field, $mock_instance, $view_mode, $form, $form_state);
  287. }
  288. }
  289. }
  290. /**
  291. * Implements hook_file_formatter_FORMATTER_view().
  292. *
  293. * Returns a drupal_render() array to display an image of the chosen style.
  294. *
  295. * This formatter is only capable of displaying local images. If the passed in
  296. * file is either not local or not an image, nothing is returned, so that
  297. * file_view_file() can try another formatter.
  298. */
  299. function file_entity_file_formatter_file_image_view($file, $display, $langcode) {
  300. // Prevent PHP notices when trying to read empty files.
  301. // @see http://drupal.org/node/681042
  302. if (!filesize($file->uri)) {
  303. return;
  304. }
  305. // Do not bother proceeding if this file does not have an image mime type.
  306. if (strpos($file->filemime, 'image/') !== 0) {
  307. return;
  308. }
  309. if (file_entity_file_is_local($file) && $image = image_load($file->uri)) {
  310. if (!empty($display['settings']['image_style'])) {
  311. $element = array(
  312. '#theme' => 'image_style',
  313. '#style_name' => $display['settings']['image_style'],
  314. '#path' => $file->uri,
  315. '#width' => $image->info['width'],
  316. '#height' => $image->info['height'],
  317. );
  318. }
  319. else {
  320. $element = array(
  321. '#theme' => 'image',
  322. '#path' => $file->uri,
  323. '#width' => $image->info['width'],
  324. '#height' => $image->info['height'],
  325. );
  326. }
  327. return $element;
  328. }
  329. }
  330. /**
  331. * Implements hook_file_formatter_FORMATTER_settings().
  332. *
  333. * Returns form elements for configuring the 'file_image' formatter.
  334. */
  335. function file_entity_file_formatter_file_image_settings($form, &$form_state, $settings) {
  336. $element = array();
  337. $element['image_style'] = array(
  338. '#title' => t('Image style'),
  339. '#type' => 'select',
  340. '#options' => image_style_options(FALSE),
  341. '#default_value' => $settings['image_style'],
  342. '#empty_option' => t('None (original image)'),
  343. );
  344. return $element;
  345. }
  346. /**
  347. * Menu access callback for the 'view mode file display settings' pages.
  348. *
  349. * Based on _field_ui_view_mode_menu_access(), but the Field UI module might not
  350. * be enabled.
  351. */
  352. function _file_entity_view_mode_menu_access($bundle, $view_mode, $access_callback) {
  353. // Deny access if the view mode isn't configured to use custom display
  354. // settings.
  355. $file_type = field_extract_bundle('file', $bundle);
  356. $view_mode_settings = field_view_mode_settings('file', $file_type);
  357. $visibility = ($view_mode == 'default') || !empty($view_mode_settings[$view_mode]['custom_settings']);
  358. if (!$visibility) {
  359. return FALSE;
  360. }
  361. // Otherwise, continue to an $access_callback check.
  362. $args = array_slice(func_get_args(), 3);
  363. $callback = empty($access_callback) ? 0 : trim($access_callback);
  364. if (is_numeric($callback)) {
  365. return (bool) $callback;
  366. }
  367. elseif (function_exists($access_callback)) {
  368. return call_user_func_array($access_callback, $args);
  369. }
  370. }
  371. /**
  372. * Implements hook_modules_enabled().
  373. */
  374. function file_entity_modules_enabled($modules) {
  375. file_info_cache_clear();
  376. }
  377. /**
  378. * Implements hook_modules_disabled().
  379. */
  380. function file_entity_modules_disabled($modules) {
  381. file_info_cache_clear();
  382. }
  383. /**
  384. * Implements hook_file_mimetype_mapping_alter().
  385. */
  386. function file_entity_file_mimetype_mapping_alter(&$mapping) {
  387. // Fix the mime type mapping for ogg.
  388. // @todo Remove when http://drupal.org/node/1239376 is fixed in core.
  389. $new_mappings['ogg'] = 'audio/ogg';
  390. // Add support for m4v.
  391. // @todo Remove when http://drupal.org/node/1290486 is fixed in core.
  392. $new_mappings['m4v'] = 'video/x-m4v';
  393. // Add support for mka and mkv.
  394. // @todo Remove when http://drupal.org/node/1293140 is fixed in core.
  395. $new_mappings['mka'] = 'audio/x-matroska';
  396. $new_mappings['mkv'] = 'video/x-matroska';
  397. // Add support for weba, webm, and webp.
  398. // @todo Remove when http://drupal.org/node/1347624 is fixed in core.
  399. $new_mappings['weba'] = 'audio/webm';
  400. $new_mappings['webm'] = 'video/webm';
  401. $new_mappings['webp'] = 'image/webp';
  402. foreach ($new_mappings as $extension => $mime_type) {
  403. if (!in_array($mime_type, $mapping['mimetypes'])) {
  404. // If the mime type does not already exist, add it.
  405. $mapping['mimetypes'][] = $mime_type;
  406. }
  407. // Get the index of the mime type and assign the extension to that key.
  408. $index = array_search($mime_type, $mapping['mimetypes']);
  409. $mapping['extensions'][$extension] = $index;
  410. }
  411. }
  412. /**
  413. * Check if a file entity is considered local or not.
  414. *
  415. * @param object $file
  416. * A file entity object from file_load().
  417. *
  418. * @return
  419. * TRUE if the file is using a local stream wrapper, or FALSE otherwise.
  420. */
  421. function file_entity_file_is_local($file) {
  422. $scheme = file_uri_scheme($file->uri);
  423. $wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
  424. return !empty($wrappers[$scheme]) && empty($wrappers[$scheme]['remote']);
  425. }