Issue #1845906 : Add restrict access to view private files permission
[project/file_entity.git] / file_entity.module
1 <?php
2
3 /**
4 * @file
5 * Extends Drupal file entities to be fieldable and viewable.
6 */
7
8 /**
9 * Modules should return this value from hook_file_entity_access() to allow
10 * access to a file.
11 */
12 define('FILE_ENTITY_ACCESS_ALLOW', 'allow');
13
14 /**
15 * Modules should return this value from hook_file_entity_access() to deny
16 * access to a file.
17 */
18 define('FILE_ENTITY_ACCESS_DENY', 'deny');
19
20 /**
21 * Modules should return this value from hook_file_entity_access() to not affect
22 * file access.
23 */
24 define('FILE_ENTITY_ACCESS_IGNORE', NULL);
25
26 /**
27 * As part of extending Drupal core's file entity API, this module adds some
28 * functions to the 'file' namespace. For organization, those are kept in the
29 * 'file_entity.file_api.inc' file.
30 */
31 require_once dirname(__FILE__) . '/file_entity.file_api.inc';
32
33 // @todo Remove when http://drupal.org/node/977052 is fixed.
34 require_once dirname(__FILE__) . '/file_entity.field.inc';
35
36 /**
37 * Implements hook_hook_info().
38 */
39 function file_entity_hook_info() {
40 $hooks = array(
41 'file_operation_info',
42 'file_operation_info_alter',
43 'file_type_info',
44 'file_type_info_alter',
45 'file_formatter_info',
46 'file_formatter_info_alter',
47 'file_view',
48 'file_view_alter',
49 'file_displays_alter',
50 'file_type',
51 'file_type_alter',
52 );
53
54 return array_fill_keys($hooks, array('group' => 'file'));
55 }
56
57 /**
58 * Implements hook_hook_info_alter().
59 *
60 * Add support for existing core hooks to be located in modulename.file.inc.
61 */
62 function file_entity_hook_info_alter(&$info) {
63 $hooks = array(
64 // File API hooks
65 'file_copy',
66 'file_move',
67 'file_validate',
68 // File access
69 'file_download',
70 'file_download_access',
71 'file_download_access_alter',
72 // File entity hooks
73 'file_load',
74 'file_presave',
75 'file_insert',
76 'file_update',
77 'file_delete',
78 // Miscellanious hooks
79 'file_mimetype_mapping_alter',
80 'file_url_alter',
81 );
82 $info += array_fill_keys($hooks, array('group' => 'file'));
83 }
84
85 /**
86 * Implements hook_help().
87 */
88 function file_entity_help($path, $arg) {
89 switch ($path) {
90 case 'admin/structure/file-types':
91 $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>';
92 return $output;
93 case 'admin/structure/file-types/manage/%/display/preview':
94 case 'admin/structure/file-types/manage/%/file-display/preview':
95 drupal_set_message(t('Some modules rely on the Preview view mode to function correctly. Changing these settings may break parts of your site.'), 'warning');
96 break;
97 }
98 }
99
100 /**
101 * Implements hook_menu().
102 */
103 function file_entity_menu() {
104 // File Configuration
105 // @todo Move this back to admin/config/media/file-types in Drupal 8 if
106 // MENU_MAX_DEPTH is increased to a value higher than 9.
107 $items['admin/structure/file-types'] = array(
108 'title' => 'File types',
109 'description' => 'Manage settings for the type of files used on your site.',
110 'page callback' => 'file_entity_list_types_page',
111 'access arguments' => array('administer file types'),
112 'file' => 'file_entity.admin.inc',
113 );
114 $items['admin/structure/file-types/manage/%file_type'] = array(
115 'title' => 'Manage file types',
116 'description' => 'Manage settings for the type of files used on your site.',
117 );
118 $items['admin/content/file'] = array(
119 'title' => 'Files',
120 'description' => 'Manage files used on your site.',
121 'page callback' => 'drupal_get_form',
122 'page arguments' => array('file_entity_admin_file'),
123 'access arguments' => array('administer files'),
124 'type' => MENU_LOCAL_TASK | MENU_NORMAL_ITEM,
125 'file' => 'file_entity.admin.inc',
126 );
127 $items['admin/content/file/list'] = array(
128 'title' => 'List',
129 'type' => MENU_DEFAULT_LOCAL_TASK,
130 );
131 // general view, edit, delete for files
132 $items['file/add'] = array(
133 'title' => 'Add file',
134 'page callback' => 'drupal_get_form',
135 'page arguments' => array('file_entity_add_upload', array()),
136 'access callback' => 'file_entity_access',
137 'access arguments' => array('create'),
138 'file' => 'file_entity.pages.inc',
139 );
140 if (module_exists('plupload') && module_exists('multiform')) {
141 $items['file/add']['page arguments'] = array('file_entity_add_upload_multiple');
142 }
143 $items['file/add/upload'] = array(
144 'title' => 'Upload',
145 'type' => MENU_DEFAULT_LOCAL_TASK,
146 'weight' => -10,
147 );
148 $items['file/%file'] = array(
149 'title callback' => 'entity_label',
150 'title arguments' => array('file', 1),
151 // The page callback also invokes drupal_set_title() in case
152 // the menu router's title is overridden by a menu link.
153 'page callback' => 'file_entity_view_page',
154 'page arguments' => array(1),
155 'access callback' => 'file_entity_access',
156 'access arguments' => array('view', 1),
157 'file' => 'file_entity.pages.inc',
158 );
159 $items['file/%file/view'] = array(
160 'title' => 'View',
161 'type' => MENU_DEFAULT_LOCAL_TASK,
162 'weight' => -10,
163 );
164 $items['file/%file/usage'] = array(
165 'title' => 'Usage',
166 'page callback' => 'file_entity_usage_page',
167 'page arguments' => array(1),
168 'access callback' => 'file_entity_access',
169 'access arguments' => array('update', 1),
170 'type' => MENU_LOCAL_TASK,
171 'context' => MENU_CONTEXT_PAGE,
172 'file' => 'file_entity.pages.inc',
173 );
174 $items['file/%file/download'] = array(
175 'title' => 'Download',
176 'page callback' => 'file_entity_download_page',
177 'page arguments' => array(1),
178 'access callback' => 'file_entity_access',
179 'access arguments' => array('download', 1),
180 'file' => 'file_entity.pages.inc',
181 'type' => MENU_CALLBACK,
182 );
183 $items['file/%file/edit'] = array(
184 'title' => 'Edit',
185 'page callback' => 'drupal_get_form',
186 'page arguments' => array('file_entity_edit', 1),
187 'access callback' => 'file_entity_access',
188 'access arguments' => array('update', 1),
189 'weight' => 0,
190 'type' => MENU_LOCAL_TASK,
191 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
192 'file' => 'file_entity.pages.inc',
193 );
194 $items['file/%file/delete'] = array(
195 'title' => 'Delete',
196 'page callback' => 'drupal_get_form',
197 'page arguments' => array('file_entity_delete_form', 1),
198 'access callback' => 'file_entity_access',
199 'access arguments' => array('delete', 1),
200 'weight' => 1,
201 'type' => MENU_LOCAL_TASK,
202 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
203 'file' => 'file_entity.pages.inc',
204 );
205
206 // Attach a "Manage file display" tab to each file type in the same way that
207 // Field UI attaches "Manage fields" and "Manage display" tabs. Note that
208 // Field UI does not have to be enabled; we're just using the same IA pattern
209 // here for attaching the "Manage file display" page.
210 $entity_info = entity_get_info('file');
211 foreach ($entity_info['bundles'] as $file_type => $bundle_info) {
212 if (isset($bundle_info['admin'])) {
213 // Get the base path and access.
214 $path = $bundle_info['admin']['path'];
215 $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments')));
216 $access += array(
217 'access callback' => 'user_access',
218 'access arguments' => array('administer file types'),
219 );
220
221 // The file type must be passed to the page callbacks. It might be
222 // configured as a wildcard (multiple file types sharing the same menu
223 // router path).
224 $file_type_argument = isset($bundle_info['admin']['bundle argument']) ? $bundle_info['admin']['bundle argument'] : $file_type;
225
226 $items[$path] = array(
227 'title' => 'Edit file type',
228 'title callback' => 'file_entity_type_get_name',
229 'title arguments' => array(4),
230 'page callback' => 'drupal_get_form',
231 'page arguments' => array('file_entity_file_type_form', $file_type_argument),
232 'file' => 'file_entity.admin.inc',
233 ) + $access;
234
235 // Add the 'File type settings' tab.
236 $items["$path/edit"] = array(
237 'title' => 'Edit',
238 'type' => MENU_DEFAULT_LOCAL_TASK,
239 );
240
241 // Add the 'Manage file display' tab.
242 $items["$path/file-display"] = array(
243 'title' => 'Manage file display',
244 'page callback' => 'drupal_get_form',
245 'page arguments' => array('file_entity_file_display_form', $file_type_argument, 'default'),
246 'type' => MENU_LOCAL_TASK,
247 'weight' => 3,
248 'file' => 'file_entity.admin.inc',
249 ) + $access;
250
251 // Add a secondary tab for each view mode.
252 $weight = 0;
253 $view_modes = array('default' => array('label' => t('Default'))) + $entity_info['view modes'];
254 foreach ($view_modes as $view_mode => $view_mode_info) {
255 $items["$path/file-display/$view_mode"] = array(
256 'title' => $view_mode_info['label'],
257 'page arguments' => array('file_entity_file_display_form', $file_type_argument, $view_mode),
258 'type' => ($view_mode == 'default' ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK),
259 'weight' => ($view_mode == 'default' ? -10 : $weight++),
260 'file' => 'file_entity.admin.inc',
261 // View modes for which the 'custom settings' flag isn't TRUE are
262 // disabled via this access callback. This needs to extend, rather
263 // than override normal $access rules.
264 'access callback' => '_file_entity_view_mode_menu_access',
265 'access arguments' => array_merge(array($file_type_argument, $view_mode, $access['access callback']), $access['access arguments']),
266 );
267 }
268 }
269 }
270
271 // Optional devel module integration
272 if (module_exists('devel')) {
273 $items['file/%file/devel'] = array(
274 'title' => 'Devel',
275 'page callback' => 'devel_load_object',
276 'page arguments' => array('file', 1),
277 'access arguments' => array('access devel information'),
278 'type' => MENU_LOCAL_TASK,
279 'file' => 'devel.pages.inc',
280 'file path' => drupal_get_path('module', 'devel'),
281 'weight' => 100,
282 );
283 $items['file/%file/devel/load'] = array(
284 'title' => 'Load',
285 'type' => MENU_DEFAULT_LOCAL_TASK,
286 );
287 $items['file/%file/devel/render'] = array(
288 'title' => 'Render',
289 'page callback' => 'devel_render_object',
290 'page arguments' => array('file', 1),
291 'access arguments' => array('access devel information'),
292 'file' => 'devel.pages.inc',
293 'file path' => drupal_get_path('module', 'devel'),
294 'type' => MENU_LOCAL_TASK,
295 'weight' => 100,
296 );
297 if (module_exists('token')) {
298 $items['file/%file/devel/token'] = array(
299 'title' => 'Tokens',
300 'page callback' => 'token_devel_token_object',
301 'page arguments' => array('file', 1),
302 'access arguments' => array('access devel information'),
303 'type' => MENU_LOCAL_TASK,
304 'file' => 'token.pages.inc',
305 'file path' => drupal_get_path('module', 'token'),
306 'weight' => 5,
307 );
308 }
309 }
310
311 return $items;
312 }
313
314 /**
315 * Implements hook_menu_local_tasks_alter().
316 */
317 function file_entity_menu_local_tasks_alter(&$data, $router_item, $root_path) {
318 // Add action link to 'file/add' on 'admin/content/file' page.
319 if ($root_path == 'admin/content/file') {
320 $item = menu_get_item('file/add');
321 if (!empty($item['access'])) {
322 $data['actions']['output'][] = array(
323 '#theme' => 'menu_local_action',
324 '#link' => $item,
325 '#weight' => $item['weight'],
326 );
327 }
328 }
329 }
330
331 /**
332 * Implement hook_permission().
333 */
334 function file_entity_permission() {
335 $permissions = array(
336 'bypass file access' => array(
337 'title' => t('Bypass file access control'),
338 'description' => t('View, edit and delete all files regardless of permission restrictions.'),
339 'restrict access' => TRUE,
340 ),
341 'administer file types' => array(
342 'title' => t('Administer file types'),
343 'restrict access' => TRUE,
344 ),
345 'administer files' => array(
346 'title' => t('Administer files'),
347 'restrict access' => TRUE,
348 ),
349 'create files' => array(
350 'title' => t('Add and upload new files'),
351 ),
352 'view own private files' => array(
353 'title' => t('View own private files'),
354 ),
355 'view own files' => array(
356 'title' => t('View own files'),
357 ),
358 'view private files' => array(
359 'title' => t('View private files'),
360 'restrict access' => TRUE,
361 ),
362 'view files' => array(
363 'title' => t('View files'),
364 ),
365 'edit own files' => array(
366 'title' => t('Edit own files'),
367 ),
368 'edit any files' => array(
369 'title' => t('Edit any files'),
370 ),
371 'delete own files' => array(
372 'title' => t('Delete own files'),
373 ),
374 'delete any files' => array(
375 'title' => t('Delete any files'),
376 ),
377 'download own files' => array(
378 'title' => t('Download own files'),
379 ),
380 'download any files' => array(
381 'title' => t('Download any files'),
382 ),
383 );
384
385 // Add description for the 'View file details' and 'View own private file
386 // details' permissions to show which stream wrappers they apply to.
387 $wrappers = array();
388 foreach (file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE) as $key => $wrapper) {
389 if (empty($wrapper['private'])) {
390 $wrappers['public'][$key] = $wrapper['name'];
391 }
392 else {
393 $wrappers['private'][$key] = $wrapper['name'];
394 }
395 }
396 $wrappers += array('public' => array(t('None')), 'private' => array(t('None')));
397
398 $permissions['view files']['description'] = t('Includes the following stream wrappers: %wrappers.', array('%wrappers' => implode(', ', $wrappers['public'])));
399 $permissions['view own private files']['description'] = t('Includes the following stream wrappers: %wrappers.', array('%wrappers' => implode(', ', $wrappers['private'])));
400
401 return $permissions;
402 }
403
404 /**
405 * Gather the rankings from the the hook_ranking implementations.
406 *
407 * @param $query
408 * A query object that has been extended with the Search DB Extender.
409 */
410 function _file_entity_rankings(SelectQueryExtender $query) {
411 if ($ranking = module_invoke_all('file_ranking')) {
412 $tables = &$query->getTables();
413 foreach ($ranking as $rank => $values) {
414 if ($file_rank = variable_get('file_entity_rank_' . $rank, 0)) {
415 // If the table defined in the ranking isn't already joined, then add it.
416 if (isset($values['join']) && !isset($tables[$values['join']['alias']])) {
417 $query->addJoin($values['join']['type'], $values['join']['table'], $values['join']['alias'], $values['join']['on']);
418 }
419 $arguments = isset($values['arguments']) ? $values['arguments'] : array();
420 $query->addScore($values['score'], $arguments, $file_rank);
421 }
422 }
423 }
424 }
425
426 /**
427 * Implements hook_search_info().
428 */
429 function file_entity_search_info() {
430 return array(
431 'title' => 'Files',
432 'path' => 'file',
433 );
434 }
435
436 /**
437 * Implements hook_search_access().
438 */
439 function file_entity_search_access() {
440 return user_access('view own private files') || user_access('view own files') || user_access('view private files') || user_access('view files');
441 }
442
443 /**
444 * Implements hook_search_reset().
445 */
446 function file_entity_search_reset() {
447 db_update('search_dataset')
448 ->fields(array('reindex' => REQUEST_TIME))
449 ->condition('type', 'file')
450 ->execute();
451 }
452
453 /**
454 * Implements hook_search_status().
455 */
456 function file_entity_search_status() {
457 $total = db_query('SELECT COUNT(*) FROM {file_managed}')->fetchField();
458 $remaining = db_query("SELECT COUNT(*) FROM {file_managed} fm LEFT JOIN {search_dataset} d ON d.type = 'file' AND d.sid = fm.fid WHERE d.sid IS NULL OR d.reindex <> 0")->fetchField();
459 return array('remaining' => $remaining, 'total' => $total);
460 }
461
462 /**
463 * Implements hook_search_admin().
464 */
465 function file_entity_search_admin() {
466 // Output form for defining rank factor weights.
467 $form['file_ranking'] = array(
468 '#type' => 'fieldset',
469 '#title' => t('File ranking'),
470 );
471 $form['file_ranking']['#theme'] = 'file_entity_search_admin';
472 $form['file_ranking']['info'] = array(
473 '#value' => '<em>' . t('The following numbers control which properties the file search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '</em>'
474 );
475
476 // Note: reversed to reflect that higher number = higher ranking.
477 $options = drupal_map_assoc(range(0, 10));
478 foreach (module_invoke_all('file_ranking') as $var => $values) {
479 $form['file_ranking']['factors']['file_entity_rank_' . $var] = array(
480 '#title' => $values['title'],
481 '#type' => 'select',
482 '#options' => $options,
483 '#default_value' => variable_get('file_entity_rank_' . $var, 0),
484 );
485 }
486 return $form;
487 }
488
489 /**
490 * Implements hook_search_execute().
491 */
492 function file_entity_search_execute($keys = NULL, $conditions = NULL) {
493 global $user;
494
495 // Build matching conditions
496 $query = db_select('search_index', 'i', array('target' => 'slave'))->extend('SearchQuery')->extend('PagerDefault');
497 $query->join('file_managed', 'fm', 'fm.fid = i.sid');
498 $query->searchExpression($keys, 'file');
499
500 if (user_access('bypass file access')) {
501 // Administrators don't need to be restricted to only permanent files.
502 $query->condition('fm.status', FILE_STATUS_PERMANENT);
503 }
504 elseif (user_access('view files')) {
505 // For non-private files, users can view if they have the 'view files'
506 // permission.
507 $query->condition('fm.status', FILE_STATUS_PERMANENT);
508 }
509 elseif (user_access('view private files') && user_is_logged_in()) {
510 // For private files, users can view private files if the
511 // user is not anonymous, and has the 'view private files' permission.
512 $query->condition('fm.uri', db_like('private://') . '%', 'LIKE');
513 $query->condition('fm.status', FILE_STATUS_PERMANENT);
514 }
515 elseif (user_access('view own files')) {
516 // For non-private files, allow to see if user owns the file.
517 $query->condition('fm.uid', $user->uid, '=');
518 $query->condition('fm.status', FILE_STATUS_PERMANENT);
519 }
520 elseif (user_access('view own private files') && user_is_logged_in()) {
521 // For private files, users can view their own private files if the
522 // user is not anonymous, and has the 'view own private files' permission.
523 $query->condition('fm.uri', db_like('private://') . '%', 'LIKE');
524 $query->condition('fm.uid', $user->uid, '=');
525 $query->condition('fm.status', FILE_STATUS_PERMANENT);
526 }
527
528 // Insert special keywords.
529 $query->setOption('type', 'fm.type');
530 if ($query->setOption('term', 'ti.tid')) {
531 $query->join('taxonomy_index', 'ti', 'fm.fid = ti.fid');
532 }
533 // Only continue if the first pass query matches.
534 if (!$query->executeFirstPass()) {
535 return array();
536 }
537
538 // Add the ranking expressions.
539 _file_entity_rankings($query);
540
541 // Load results.
542 $find = $query
543 ->limit(10)
544 ->execute();
545 $results = array();
546 foreach ($find as $item) {
547 // Render the file.
548 $file = file_load($item->sid);
549 $build = file_view($file, 'search_result');
550 unset($build['#theme']);
551 $file->rendered = drupal_render($build);
552
553 $extra = module_invoke_all('file_entity_search_result', $file);
554
555 $types = file_entity_type_get_names();
556
557 $uri = entity_uri('file', $file);
558 $results[] = array(
559 'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE))),
560 'type' => check_plain($types[$file->type]),
561 'title' => $file->filename,
562 'user' => theme('username', array('account' => user_load($file->uid))),
563 'date' => $file->timestamp,
564 'file' => $file,
565 'extra' => $extra,
566 'score' => $item->calculated_score,
567 'snippet' => search_excerpt($keys, $file->rendered),
568 'language' => function_exists('entity_language') ? entity_language('file', $file) : NULL,
569 );
570 }
571 return $results;
572 }
573
574 /**
575 * Implements hook_file_ranking().
576 */
577 function file_entity_file_ranking() {
578 // Create the ranking array and add the basic ranking options.
579 $ranking = array(
580 'relevance' => array(
581 'title' => t('Keyword relevance'),
582 // Average relevance values hover around 0.15
583 'score' => 'i.relevance',
584 ),
585 );
586
587 // Add relevance based on creation date.
588 if ($file_cron_last = variable_get('file_entity_cron_last', 0)) {
589 $ranking['timestamp'] = array(
590 'title' => t('Recently posted'),
591 // Exponential decay with half-life of 6 months, starting at last indexed file
592 'score' => 'POW(2.0, (fm.timestamp - :file_cron_last) * 6.43e-8)',
593 'arguments' => array(':file_cron_last' => $file_cron_last),
594 );
595 }
596 return $ranking;
597 }
598
599 /**
600 * Returns HTML for the file ranking part of the search settings admin page.
601 *
602 * @param $variables
603 * An associative array containing:
604 * - form: A render element representing the form.
605 *
606 * @ingroup themeable
607 */
608 function theme_file_entity_search_admin($variables) {
609 $form = $variables['form'];
610
611 $output = drupal_render($form['info']);
612
613 $header = array(t('Factor'), t('Weight'));
614 foreach (element_children($form['factors']) as $key) {
615 $row = array();
616 $row[] = $form['factors'][$key]['#title'];
617 $form['factors'][$key]['#title_display'] = 'invisible';
618 $row[] = drupal_render($form['factors'][$key]);
619 $rows[] = $row;
620 }
621 $output .= theme('table', array('header' => $header, 'rows' => $rows));
622
623 $output .= drupal_render_children($form);
624 return $output;
625 }
626
627 /**
628 * Implements hook_update_index().
629 */
630 function file_entity_update_index() {
631 $limit = (int)variable_get('search_cron_limit', 100);
632
633 $result = db_query_range("SELECT fm.fid FROM {file_managed} fm LEFT JOIN {search_dataset} d ON d.type = 'file' AND d.sid = fm.fid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, fm.fid ASC", 0, $limit, array(), array('target' => 'slave'));
634
635 foreach ($result as $file) {
636 _file_entity_index_file($file);
637 }
638 }
639
640 /**
641 * Index a single file.
642 *
643 * @param $file
644 * The file to index.
645 */
646 function _file_entity_index_file($file) {
647 $file = file_load($file->fid);
648
649 // Save the creation time of the most recent indexed file, for the search
650 // results half-life calculation.
651 variable_set('file_entity_cron_last', $file->timestamp);
652
653 // Render the file.
654 $build = file_view($file, 'search_index');
655 unset($build['#theme']);
656 $file->rendered = drupal_render($build);
657
658 $text = '<h1>' . check_plain($file->filename) . '</h1>' . $file->rendered;
659
660 // Fetch extra data normally not visible
661 $extra = module_invoke_all('file_entity_update_index', $file);
662 foreach ($extra as $t) {
663 $text .= $t;
664 }
665
666 // Update index
667 search_index($file->fid, 'file', $text);
668 }
669
670 /**
671 * Implements hook_form_FORM_ID_alter().
672 */
673 function file_entity_form_search_form_alter(&$form, $form_state) {
674 if (isset($form['module']) && $form['module']['#value'] == 'file_entity' && user_access('use advanced search')) {
675 // Keyword boxes:
676 $form['advanced'] = array(
677 '#type' => 'fieldset',
678 '#title' => t('Advanced search'),
679 '#collapsible' => TRUE,
680 '#collapsed' => TRUE,
681 '#attributes' => array('class' => array('search-advanced')),
682 );
683 $form['advanced']['keywords'] = array(
684 '#prefix' => '<div class="criterion">',
685 '#suffix' => '</div>',
686 );
687 $form['advanced']['keywords']['or'] = array(
688 '#type' => 'textfield',
689 '#title' => t('Containing any of the words'),
690 '#size' => 30,
691 '#maxlength' => 255,
692 );
693 $form['advanced']['keywords']['phrase'] = array(
694 '#type' => 'textfield',
695 '#title' => t('Containing the phrase'),
696 '#size' => 30,
697 '#maxlength' => 255,
698 );
699 $form['advanced']['keywords']['negative'] = array(
700 '#type' => 'textfield',
701 '#title' => t('Containing none of the words'),
702 '#size' => 30,
703 '#maxlength' => 255,
704 );
705
706 // File types:
707 $types = array_map('check_plain', file_entity_type_get_names());
708 $form['advanced']['type'] = array(
709 '#type' => 'checkboxes',
710 '#title' => t('Only of the type(s)'),
711 '#prefix' => '<div class="criterion">',
712 '#suffix' => '</div>',
713 '#options' => $types,
714 );
715 $form['advanced']['submit'] = array(
716 '#type' => 'submit',
717 '#value' => t('Advanced search'),
718 '#prefix' => '<div class="action">',
719 '#suffix' => '</div>',
720 '#weight' => 100,
721 );
722
723 $form['#validate'][] = 'file_entity_search_validate';
724 }
725 }
726
727 /**
728 * Form API callback for the search form. Registered in file_entity_form_alter().
729 */
730 function file_entity_search_validate($form, &$form_state) {
731 // Initialize using any existing basic search keywords.
732 $keys = $form_state['values']['processed_keys'];
733
734 // Insert extra restrictions into the search keywords string.
735 if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) {
736 // Retrieve selected types - Form API sets the value of unselected
737 // checkboxes to 0.
738 $form_state['values']['type'] = array_filter($form_state['values']['type']);
739 if (count($form_state['values']['type'])) {
740 $keys = search_expression_insert($keys, 'type', implode(',', array_keys($form_state['values']['type'])));
741 }
742 }
743
744 if (isset($form_state['values']['term']) && is_array($form_state['values']['term']) && count($form_state['values']['term'])) {
745 $keys = search_expression_insert($keys, 'term', implode(',', $form_state['values']['term']));
746 }
747 if ($form_state['values']['or'] != '') {
748 if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['or'], $matches)) {
749 $keys .= ' ' . implode(' OR ', $matches[1]);
750 }
751 }
752 if ($form_state['values']['negative'] != '') {
753 if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['negative'], $matches)) {
754 $keys .= ' -' . implode(' -', $matches[1]);
755 }
756 }
757 if ($form_state['values']['phrase'] != '') {
758 $keys .= ' "' . str_replace('"', ' ', $form_state['values']['phrase']) . '"';
759 }
760 if (!empty($keys)) {
761 form_set_value($form['basic']['processed_keys'], trim($keys), $form_state);
762 }
763 }
764
765 /**
766 * Implements hook_admin_paths().
767 */
768 function file_entity_admin_paths() {
769 $paths = array(
770 'file/add' => TRUE,
771 'file/add/*' => TRUE,
772 'file/*/edit' => TRUE,
773 'file/*/usage' => TRUE,
774 'file/*/delete' => TRUE,
775 );
776 return $paths;
777 }
778
779 /**
780 * Implements hook_theme().
781 */
782 function file_entity_theme() {
783 return array(
784 'file_entity' => array(
785 'render element' => 'elements',
786 'template' => 'file_entity',
787 ),
788 'file_entity_search_admin' => array(
789 'render element' => 'form',
790 ),
791 'file_entity_file_type_overview' => array(
792 'variables' => array('label' => NULL, 'description' => NULL),
793 'file' => 'file_entity.admin.inc',
794 ),
795 'file_entity_file_display_order' => array(
796 'render element' => 'element',
797 'file' => 'file_entity.admin.inc',
798 ),
799 'file_entity_file_link' => array(
800 'variables' => array('file' => NULL, 'icon_directory' => NULL),
801 'file' => 'file_entity.theme.inc',
802 ),
803 );
804 }
805
806 /**
807 * Implements hook_entity_info_alter().
808 *
809 * Extends the core file entity to be fieldable. The file type is used as the
810 * bundle key. File types are implemented as CTools exportables, so modules can
811 * define default file types via hook_file_default_types(), and the
812 * administrator can override the default types or add custom ones via
813 * admin/structure/file-types.
814 */
815 function file_entity_entity_info_alter(&$entity_info) {
816 $entity_info['file']['fieldable'] = TRUE;
817 $entity_info['file']['entity keys']['bundle'] = 'type';
818 $entity_info['file']['bundle keys']['bundle'] = 'type';
819 $entity_info['file']['bundles'] = array();
820 $entity_info['file']['uri callback'] = 'file_entity_uri';
821 $entity_info['file']['view modes']['teaser'] = array(
822 'label' => t('Teaser'),
823 'custom settings' => TRUE,
824 );
825 $entity_info['file']['view modes']['full'] = array(
826 'label' => t('Full content'),
827 'custom settings' => FALSE,
828 );
829 $entity_info['file']['view modes']['preview'] = array(
830 'label' => t('Preview'),
831 'custom settings' => TRUE,
832 );
833
834 // Search integration is provided by file_entity.module, so search-related
835 // view modes for files are defined here and not in search.module.
836 if (module_exists('search')) {
837 $entity_info['file']['view modes']['search_index'] = array(
838 'label' => t('Search index'),
839 'custom settings' => FALSE,
840 );
841 $entity_info['file']['view modes']['search_result'] = array(
842 'label' => t('Search result'),
843 'custom settings' => FALSE,
844 );
845 }
846
847 foreach (file_type_get_enabled_types() as $type) {
848 $entity_info['file']['bundles'][$type->type] = array(
849 'label' => $type->label,
850 'admin' => array(
851 'path' => 'admin/structure/file-types/manage/%file_type',
852 'real path' => 'admin/structure/file-types/manage/' . $type->type,
853 'bundle argument' => 4,
854 ),
855 );
856 }
857
858 // Enable Metatag support.
859 $entity_info['file']['metatags'] = TRUE;
860
861 // Ensure some of the Entity API callbacks are supported.
862 $entity_info['file']['creation callback'] = 'entity_metadata_create_object';
863 $entity_info['file']['view callback'] = 'file_view_multiple';
864 $entity_info['file']['edit callback'] = 'file_entity_metadata_form_file';
865 $entity_info['file']['access callback'] = 'file_entity_access';
866
867 // Add integration with the Title module for file name replacement support.
868 $entity_info['file']['field replacement'] = array(
869 'filename' => array(
870 'field' => array(
871 'type' => 'text',
872 'cardinality' => 1,
873 'translatable' => TRUE,
874 ),
875 'instance' => array(
876 'label' => t('File name'),
877 'description' => t('A field replacing file name.'),
878 'required' => TRUE,
879 'settings' => array(
880 'text_processing' => 0,
881 ),
882 'widget' => array(
883 'weight' => -5,
884 ),
885 'display' => array(
886 'default' => array(
887 'type' => 'hidden',
888 ),
889 ),
890 ),
891 'preprocess_key' => 'filename',
892 ),
893 );
894 }
895
896 /**
897 * Implements hook_entity_property_info().
898 */
899 function file_entity_entity_property_info() {
900 $info['file']['properties']['type'] = array(
901 'label' => t('File type'),
902 'type' => 'token',
903 'description' => t('The type of the file.'),
904 'setter callback' => 'entity_property_verbatim_set',
905 'setter permission' => 'administer files',
906 'options list' => 'file_entity_type_get_names',
907 'required' => TRUE,
908 'schema field' => 'type',
909 );
910
911 return $info;
912 }
913
914 /**
915 * Implements hook_field_display_ENTITY_TYPE_alter().
916 */
917 function file_entity_field_display_file_alter(&$display, $context) {
918 // Hide field labels in search index.
919 if ($context['view_mode'] == 'search_index') {
920 $display['label'] = 'hidden';
921 }
922 }
923
924 /**
925 * URI callback for file entities.
926 */
927 function file_entity_uri($file) {
928 $uri['path'] = 'file/' . $file->fid;
929 return $uri;
930 }
931
932 /**
933 * Entity API callback to get the form of a file entity.
934 */
935 function file_entity_metadata_form_file($file) {
936 // Pre-populate the form-state with the right form include.
937 $form_state['build_info']['args'] = array($file);
938 form_load_include($form_state, 'inc', 'file_entity', 'file_entity.pages');
939 return drupal_build_form('file_entity_edit', $form_state);
940 }
941
942 /**
943 * Implements hook_field_extra_fields().
944 *
945 * Adds 'file' as an extra field, so that its display and form component can be
946 * weighted relative to the fields that are added to file entity bundles.
947 */
948 function file_entity_field_extra_fields() {
949 $info = array();
950
951 if ($file_type_names = file_entity_type_get_names()) {
952 foreach ($file_type_names as $type => $name) {
953 $info['file'][$type]['form']['filename'] = array(
954 'label' => t('File name'),
955 'description' => t('File name'),
956 'weight' => -10,
957 );
958 $info['file'][$type]['form']['preview'] = array(
959 'label' => t('File'),
960 'description' => t('File preview'),
961 'weight' => -5,
962 );
963 $info['file'][$type]['display']['file'] = array(
964 'label' => t('File'),
965 'description' => t('File display'),
966 'weight' => 0,
967 );
968 }
969 }
970
971 return $info;
972 }
973
974 /**
975 * Implements hook_file_formatter_info().
976 */
977 function file_entity_file_formatter_info() {
978 $formatters = array();
979
980 // Allow file field formatters to be reused for displaying the file entity's
981 // file pseudo-field.
982 foreach (field_info_formatter_types() as $field_formatter_type => $field_formatter_info) {
983 $is_file_formatter = in_array('file', $field_formatter_info['field types']);
984 $is_image_formatter = in_array('image', $field_formatter_info['field types']);
985
986 if ($is_file_formatter || $is_image_formatter) {
987 $formatters['file_field_' . $field_formatter_type] = array(
988 'label' => $field_formatter_info['label'],
989 'view callback' => 'file_entity_file_formatter_file_field_view',
990 );
991 if (!$is_file_formatter) {
992 $formatters['file_field_' . $field_formatter_type]['file types'] = array('image');
993 }
994 if (!empty($field_formatter_info['settings'])) {
995 $formatters['file_field_' . $field_formatter_type] += array(
996 'default settings' => $field_formatter_info['settings'],
997 'settings callback' => 'file_entity_file_formatter_file_field_settings',
998 );
999 }
1000 }
1001 }
1002
1003 // Add a simple file formatter for displaying an image in a chosen style.
1004 if (module_exists('image')) {
1005 $formatters['file_image'] = array(
1006 'label' => t('Image'),
1007 'default settings' => array(
1008 'image_style' => '',
1009 'alt' => '',
1010 'title' => ''
1011 ),
1012 'view callback' => 'file_entity_file_formatter_file_image_view',
1013 'settings callback' => 'file_entity_file_formatter_file_image_settings',
1014 );
1015 // Provide default token values.
1016 if (module_exists('token')) {
1017 $formatters['file_image']['default settings']['alt'] = '[file:field_file_image_alt_text]';
1018 $formatters['file_image']['default settings']['title'] = '[file:field_file_image_title_text]';
1019 }
1020 elseif (module_exists('entity_token')) {
1021 $formatters['file_image']['default settings']['alt'] = '[file:field-file-image-alt-text]';
1022 $formatters['file_image']['default settings']['title'] = '[file:field-file-image-title-text]';
1023 }
1024 }
1025
1026 return $formatters;
1027 }
1028
1029 /**
1030 * Implements hook_file_formatter_FORMATTER_view().
1031 *
1032 * This function provides a bridge to the field formatter API, so that file
1033 * field formatters can be reused for displaying the file entity's file
1034 * pseudo-field.
1035 */
1036 function file_entity_file_formatter_file_field_view($file, $display, $langcode) {
1037 if (strpos($display['type'], 'file_field_') === 0) {
1038 $field_formatter_type = substr($display['type'], strlen('file_field_'));
1039 $field_formatter_info = field_info_formatter_types($field_formatter_type);
1040 if (isset($field_formatter_info['module'])) {
1041 // Set $display['type'] to what hook_field_formatter_*() expects.
1042 $display['type'] = $field_formatter_type;
1043
1044 // Set $items to what file field formatters expect. See file_field_load(),
1045 // and note that, here, $file is already a fully loaded entity.
1046 $items = array((array) $file);
1047
1048 // Invoke hook_field_formatter_prepare_view() and
1049 // hook_field_formatter_view(). Note that we are reusing field formatter
1050 // functions, but we are not displaying a Field API field, so we set
1051 // $field and $instance accordingly, and do not invoke
1052 // hook_field_prepare_view(). This assumes that the formatter functions do
1053 // not rely on $field or $instance. A module that implements formatter
1054 // functions that rely on $field or $instance (and therefore, can only be
1055 // used for real fields) can prevent this formatter from being used on the
1056 // pseudo-field by removing it within hook_file_formatter_info_alter().
1057 $field = $instance = NULL;
1058 if (($function = ($field_formatter_info['module'] . '_field_formatter_prepare_view')) && function_exists($function)) {
1059 $fid = $file->fid;
1060 // hook_field_formatter_prepare_view() alters $items by reference.
1061 $grouped_items = array($fid => &$items);
1062 $function('file', array($fid => $file), $field, array($fid => $instance), $langcode, $grouped_items, array($fid => $display));
1063 }
1064 if (($function = ($field_formatter_info['module'] . '_field_formatter_view')) && function_exists($function)) {
1065 $element = $function('file', $file, $field, $instance, $langcode, $items, $display);
1066 // We passed the file as $items[0], so return the corresponding element.
1067 if (isset($element[0])) {
1068 return $element[0];
1069 }
1070 }
1071 }
1072 }
1073 }
1074
1075 /**
1076 * Implements hook_file_formatter_FORMATTER_settings().
1077 *
1078 * This function provides a bridge to the field formatter API, so that file
1079 * field formatters can be reused for displaying the file entity's file
1080 * pseudo-field.
1081 */
1082 function file_entity_file_formatter_file_field_settings($form, &$form_state, $settings, $formatter_type, $file_type, $view_mode) {
1083 if (strpos($formatter_type, 'file_field_') === 0) {
1084 $field_formatter_type = substr($formatter_type, strlen('file_field_'));
1085 $field_formatter_info = field_info_formatter_types($field_formatter_type);
1086
1087 // Invoke hook_field_formatter_settings_form(). We are reusing field
1088 // formatter functions, but we are not working with a Field API field, so
1089 // set $field accordingly. Unfortunately, the API is for $settings to be
1090 // transfered via the $instance parameter, so we must mock it.
1091 if (isset($field_formatter_info['module']) && ($function = ($field_formatter_info['module'] . '_field_formatter_settings_form')) && function_exists($function)) {
1092 $field = NULL;
1093 $mock_instance['display'][$view_mode] = array(
1094 'type' => $field_formatter_type,
1095 'settings' => $settings,
1096 );
1097 return $function($field, $mock_instance, $view_mode, $form, $form_state);
1098 }
1099 }
1100 }
1101
1102 /**
1103 * Implements hook_file_formatter_FORMATTER_view().
1104 *
1105 * Returns a drupal_render() array to display an image of the chosen style.
1106 *
1107 * This formatter is only capable of displaying local images. If the passed in
1108 * file is either not local or not an image, nothing is returned, so that
1109 * file_view_file() can try another formatter.
1110 */
1111 function file_entity_file_formatter_file_image_view($file, $display, $langcode) {
1112 // Prevent PHP notices when trying to read empty files.
1113 // @see http://drupal.org/node/681042
1114 if (!$file->filesize) {
1115 return;
1116 }
1117
1118 // Do not bother proceeding if this file does not have an image mime type.
1119 if (strpos($file->filemime, 'image/') !== 0) {
1120 return;
1121 }
1122
1123 if (file_entity_file_is_readable($file) && isset($file->image_dimensions)) {
1124 // We don't sanitize here.
1125 // @see http://drupal.org/node/1553094#comment-6257382
1126 // Theme function will take care of escaping.
1127 $replace_options = array(
1128 'clear' => 1,
1129 'sanitize' => 0,
1130 );
1131 if (!empty($display['settings']['image_style'])) {
1132 $element = array(
1133 '#theme' => 'image_style',
1134 '#style_name' => $display['settings']['image_style'],
1135 '#path' => $file->uri,
1136 '#width' => $file->image_dimensions['width'],
1137 '#height' => $file->image_dimensions['height'],
1138 '#alt' => token_replace($display['settings']['alt'], array('file' => $file), $replace_options),
1139 '#title' => token_replace($display['settings']['title'], array('file' => $file), $replace_options),
1140 );
1141 }
1142 else {
1143 $element = array(
1144 '#theme' => 'image',
1145 '#path' => $file->uri,
1146 '#width' => $file->image_dimensions['width'],
1147 '#height' => $file->image_dimensions['height'],
1148 '#alt' => token_replace($display['settings']['alt'], array('file' => $file), $replace_options),
1149 '#title' => token_replace($display['settings']['title'], array('file' => $file), $replace_options),
1150 );
1151 }
1152 return $element;
1153 }
1154 }
1155
1156 /**
1157 * Check if a file entity is readable or not.
1158 *
1159 * @param object $file
1160 * A file entity object from file_load().
1161 *
1162 * @return boolean
1163 * TRUE if the file is using a readable stream wrapper, or FALSE otherwise.
1164 */
1165 function file_entity_file_is_readable($file) {
1166 $scheme = file_uri_scheme($file->uri);
1167 $wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_READ);
1168 return !empty($wrappers[$scheme]);
1169 }
1170
1171 /**
1172 * Implements hook_file_formatter_FORMATTER_settings().
1173 *
1174 * Returns form elements for configuring the 'file_image' formatter.
1175 */
1176 function file_entity_file_formatter_file_image_settings($form, &$form_state, $settings) {
1177 $element = array();
1178 $element['image_style'] = array(
1179 '#title' => t('Image style'),
1180 '#type' => 'select',
1181 '#options' => image_style_options(FALSE),
1182 '#default_value' => $settings['image_style'],
1183 '#empty_option' => t('None (original image)'),
1184 );
1185
1186 // For image files we allow the alt attribute (required in HTML).
1187 $element['alt'] = array(
1188 '#title' => t('Alt attribute'),
1189 '#description' => t('The text to use as value for the <em>img</em> tag <em>alt</em> attribute.'),
1190 '#type' => 'textfield',
1191 '#default_value' => $settings['alt'],
1192 );
1193
1194 // Allow the setting of the title attribute.
1195 $element['title'] = array(
1196 '#title' => t('Title attribute'),
1197 '#description' => t('The text to use as value for the <em>img</em> tag <em>title</em> attribute.'),
1198 '#type' => 'textfield',
1199 '#default_value' => $settings['title'],
1200 );
1201
1202 if (module_exists('token')) {
1203 $element['alt']['#description'] .= t('This field supports tokens.');
1204 $element['title']['#description'] .= t('This field supports tokens.');
1205 $element['tokens'] = array(
1206 '#theme' => 'token_tree',
1207 '#token_types' => array('file'),
1208 '#dialog' => TRUE,
1209 );
1210 }
1211
1212 return $element;
1213 }
1214
1215 /**
1216 * Menu access callback for the 'view mode file display settings' pages.
1217 *
1218 * Based on _field_ui_view_mode_menu_access(), but the Field UI module might not
1219 * be enabled.
1220 */
1221 function _file_entity_view_mode_menu_access($file_type, $view_mode, $access_callback) {
1222 // Deny access if the view mode isn't configured to use custom display
1223 // settings.
1224 $view_mode_settings = field_view_mode_settings('file', $file_type->type);
1225 $visibility = ($view_mode == 'default') || !empty($view_mode_settings[$view_mode]['custom_settings']);
1226 if (!$visibility) {
1227 return FALSE;
1228 }
1229
1230 // Otherwise, continue to an $access_callback check.
1231 $args = array_slice(func_get_args(), 3);
1232 $callback = empty($access_callback) ? 0 : trim($access_callback);
1233 if (is_numeric($callback)) {
1234 return (bool) $callback;
1235 }
1236 elseif (function_exists($access_callback)) {
1237 return call_user_func_array($access_callback, $args);
1238 }
1239 }
1240
1241 /**
1242 * Implements hook_modules_enabled().
1243 */
1244 function file_entity_modules_enabled($modules) {
1245 file_info_cache_clear();
1246 }
1247
1248 /**
1249 * Implements hook_modules_disabled().
1250 */
1251 function file_entity_modules_disabled($modules) {
1252 file_info_cache_clear();
1253 }
1254
1255 /**
1256 * Implements hook_views_api().
1257 */
1258 function file_entity_views_api() {
1259 return array(
1260 'api' => 3,
1261 );
1262 }
1263
1264 /**
1265 * Returns whether the current page is the full page view of the passed-in file.
1266 *
1267 * @param $file
1268 * A file object.
1269 */
1270 function file_entity_is_page($file) {
1271 $page_file = menu_get_object('file', 1);
1272 return (!empty($page_file) ? $page_file->fid == $file->fid : FALSE);
1273 }
1274
1275 /**
1276 * Process variables for file_entity.tpl.php
1277 *
1278 * The $variables array contains the following arguments:
1279 * - $file
1280 * - $view_mode
1281 *
1282 * @see file_entity.tpl.php
1283 */
1284 function template_preprocess_file_entity(&$variables) {
1285 $view_mode = $variables['view_mode'] = $variables['elements']['#view_mode'];
1286 $variables['file'] = $variables['elements']['#file'];
1287 $file = $variables['file'];
1288
1289 $variables['id'] = drupal_html_id('file-'. $file->fid);
1290 $variables['date'] = format_date($file->timestamp);
1291 $account = user_load($file->uid);
1292 $variables['name'] = theme('username', array('account' => $account));
1293
1294 // @todo Use entity_uri once http://drupal.org/node/1057242 is fixed.
1295 //$uri = entity_uri('file', $file);
1296 //$variables['file_url'] = url($uri['path'], $uri['options']);
1297 $variables['file_url'] = file_create_url($file->uri);
1298 $label = entity_label('file', $file);
1299 $variables['label'] = check_plain($label);
1300 $variables['page'] = $view_mode == 'full' && file_entity_is_page($file);
1301
1302 // Hide the file name from being displayed until we can figure out a better
1303 // way to control this. We cannot simply not output the title since
1304 // contextual links require $title_suffix to be output in the template.
1305 // @see http://drupal.org/node/1245266
1306 if (!$variables['page']) {
1307 $variables['title_attributes_array']['class'][] = 'element-invisible';
1308 }
1309
1310 // Flatten the file object's member fields.
1311 $variables = array_merge((array) $file, $variables);
1312
1313 // Helpful $content variable for templates.
1314 $variables += array('content' => array());
1315 foreach (element_children($variables['elements']) as $key) {
1316 $variables['content'][$key] = $variables['elements'][$key];
1317 }
1318
1319 // Make the field variables available with the appropriate language.
1320 field_attach_preprocess('file', $file, $variables['content'], $variables);
1321
1322 // Attach the file object to the content element.
1323 $variables['content']['file']['#file'] = $file;
1324
1325 // Display post information only on certain file types.
1326 if (variable_get('file_submitted_' . $file->type, FALSE)) {
1327 $variables['display_submitted'] = TRUE;
1328 $variables['submitted'] = t('Uploaded by !username on !datetime', array('!username' => $variables['name'], '!datetime' => $variables['date']));
1329 $variables['user_picture'] = theme_get_setting('toggle_file_user_picture') ? theme('user_picture', array('account' => $account)) : '';
1330 }
1331 else {
1332 $variables['display_submitted'] = FALSE;
1333 $variables['submitted'] = '';
1334 $variables['user_picture'] = '';
1335 }
1336
1337 // Gather file classes.
1338 $variables['classes_array'][] = drupal_html_class('file-' . $file->type);
1339 $variables['classes_array'][] = drupal_html_class('file-' . $file->filemime);
1340 if ($file->status != FILE_STATUS_PERMANENT) {
1341 $variables['classes_array'][] = 'file-temporary';
1342 }
1343
1344 // Change the 'file-entity' class into 'file'
1345 if ($variables['classes_array'][0] == 'file-entity') {
1346 $variables['classes_array'][0] = 'file';
1347 }
1348
1349 // Clean up name so there are no underscores.
1350 $variables['theme_hook_suggestions'][] = 'file__' . $file->type;
1351 $variables['theme_hook_suggestions'][] = 'file__' . $file->type . '__' . $view_mode;
1352 $variables['theme_hook_suggestions'][] = 'file__' . str_replace(array('/', '-'), array('__', '_'), $file->filemime);
1353 $variables['theme_hook_suggestions'][] = 'file__' . str_replace(array('/', '-'), array('__', '_'), $file->filemime) . '__' . $view_mode;
1354 $variables['theme_hook_suggestions'][] = 'file__' . $file->fid;
1355 $variables['theme_hook_suggestions'][] = 'file__' . $file->fid . '__' . $view_mode;
1356 }
1357
1358 /**
1359 * Returns the file type name of the passed file or file type string.
1360 *
1361 * @param $file
1362 * A file object or string that indicates the file type to return.
1363 *
1364 * @return
1365 * The file type name or FALSE if the file type is not found.
1366 */
1367 function file_entity_type_get_name($file) {
1368 $type = is_object($file) ? $file->type : $file;
1369 $info = entity_get_info('file');
1370 return isset($info['bundles'][$type]['label']) ? $info['bundles'][$type]['label'] : FALSE;
1371 }
1372
1373 /**
1374 * Returns a list of available file type names.
1375 *
1376 * @return
1377 * An array of file type names, keyed by the type.
1378 */
1379 function file_entity_type_get_names() {
1380 $names = &drupal_static(__FUNCTION__);
1381
1382 if (!isset($names)) {
1383 $info = entity_get_info('file');
1384 foreach ($info['bundles'] as $bundle => $bundle_info) {
1385 $names[$bundle] = $bundle_info['label'];
1386 }
1387 }
1388
1389 return $names;
1390 }
1391
1392 /**
1393 * Return an array of available view modes for file entities.
1394 */
1395 function file_entity_view_mode_labels() {
1396 $labels = &drupal_static(__FUNCTION__);
1397
1398 if (!isset($options)) {
1399 $entity_info = entity_get_info('file');
1400 $labels = array('default' => t('Default'));
1401 foreach ($entity_info['view modes'] as $machine_name => $mode) {
1402 $labels[$machine_name] = $mode['label'];
1403 }
1404 }
1405
1406 return $labels;
1407 }
1408
1409 /**
1410 * Return the label for a specific file entity view mode.
1411 */
1412 function file_entity_view_mode_label($view_mode, $default = FALSE) {
1413 $labels = file_entity_view_mode_labels();
1414 return isset($labels[$view_mode]) ? $labels[$view_mode] : $default;
1415 }
1416
1417 /**
1418 * Helper function to get a list of hidden stream wrappers.
1419 *
1420 * This is used in several places to filter queries for media so that files in
1421 * temporary:// don't show up.
1422 */
1423 function file_entity_get_hidden_stream_wrappers() {
1424 return array_diff_key(file_get_stream_wrappers(STREAM_WRAPPERS_ALL), file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE));
1425 }
1426
1427 /**
1428 * Return a specific stream wrapper's registry information.
1429 *
1430 * @param $scheme
1431 * A URI scheme, a stream is referenced as "scheme://target".
1432 *
1433 * @see file_get_stream_wrappers()
1434 */
1435 function file_entity_get_stream_wrapper($scheme) {
1436 $wrappers = file_get_stream_wrappers();
1437 return isset($wrappers[$scheme]) ? $wrappers[$scheme] : FALSE;
1438 }
1439
1440 /**
1441 * Implements hook_stream_wrappers_alter().
1442 */
1443 function file_entity_stream_wrappers_alter(&$wrappers) {
1444 if (isset($wrappers['private'])) {
1445 $wrappers['private']['private'] = TRUE;
1446 }
1447 if (isset($wrappers['temporary'])) {
1448 $wrappers['temporary']['private'] = TRUE;
1449 }
1450 }
1451
1452 /**
1453 * Implements hook_ctools_plugin_api().
1454 */
1455 function file_entity_ctools_plugin_api($owner, $api) {
1456 if ($owner == 'file_entity' && $api == 'file_type') {
1457 return array('version' => 1);
1458 }
1459 }
1460
1461 /**
1462 * @defgroup file_entity_access File access rights
1463 * @{
1464 * The file access system determines who can do what to which files.
1465 *
1466 * In determining access rights for a file, file_entity_access() first checks
1467 * whether the user has the "bypass file access" permission. Such users have
1468 * unrestricted access to all files. user 1 will always pass this check.
1469 *
1470 * Next, all implementations of hook_file_entity_access() will be called. Each
1471 * implementation may explicitly allow, explicitly deny, or ignore the access
1472 * request. If at least one module says to deny the request, it will be rejected.
1473 * If no modules deny the request and at least one says to allow it, the request
1474 * will be permitted.
1475 *
1476 * There is no access grant system for files.
1477 *
1478 * In file listings, the process above is followed except that
1479 * hook_file_entity_access() is not called on each file for performance reasons
1480 * and for proper functioning of the pager system. When adding a filelisting to
1481 * your module, be sure to use a dynamic query created by db_select()
1482 * and add a tag of "file_entity_access". This will allow modules dealing
1483 * with file access to ensure only files to which the user has access
1484 * are retrieved, through the use of hook_query_TAG_alter().
1485 *
1486 * Note: Even a single module returning FILE_ENTITY_ACCESS_DENY from
1487 * hook_file_entity_access() will block access to the file. Therefore,
1488 * implementers should take care to not deny access unless they really intend to.
1489 * Unless a module wishes to actively deny access it should return
1490 * FILE_ENTITY_ACCESS_IGNORE (or simply return nothing)
1491 * to allow other modules to control access.
1492 *
1493 * Stream wrappers that are considered private should implement a 'private'
1494 * flag equal to TRUE in hook_stream_wrappers().
1495 *
1496 * @todo Unify core's hook_file_download() as a 'download' op of file_entity_access().
1497 */
1498
1499 /**
1500 * Determine if a user may perform the given operation on the specified file.
1501 *
1502 * @param $op
1503 * The operation to be performed on the file. Possible values are:
1504 * - "view"
1505 * - "download"
1506 * - "update"
1507 * - "delete"
1508 * - "create"
1509 * @param $file
1510 * The file object on which the operation is to be performed, or file type
1511 * (e.g. 'image') for "create" operation.
1512 * @param $account
1513 * Optional, a user object representing the user for whom the operation is to
1514 * be performed. Determines access for a user other than the current user.
1515 *
1516 * @return
1517 * TRUE if the operation may be performed, FALSE otherwise.
1518 */
1519 function file_entity_access($op, $file = NULL, $account = NULL) {
1520 $rights = &drupal_static(__FUNCTION__, array());
1521
1522 if (!$file && !in_array($op, array('view', 'download', 'update', 'delete', 'create'), TRUE)) {
1523 // If there was no file to check against, and the $op was not one of the
1524 // supported ones, we return access denied.
1525 return FALSE;
1526 }
1527
1528 // If no user object is supplied, the access check is for the current user.
1529 if (empty($account)) {
1530 $account = $GLOBALS['user'];
1531 }
1532
1533 if (!$file && $op == 'create') {
1534 return user_access('create files', $account) || user_access('bypass file access', $account);
1535 }
1536
1537 // $file may be either an object or a file type. Since file types cannot be
1538 // an integer, use either fid or type as the static cache id.
1539 $cache_id = is_object($file) ? $file->fid : $file;
1540
1541 // If we've already checked access for this file, user and op, return from
1542 // cache.
1543 if (isset($rights[$account->uid][$cache_id][$op])) {
1544 return $rights[$account->uid][$cache_id][$op];
1545 }
1546
1547 if (user_access('bypass file access', $account)) {
1548 return $rights[$account->uid][$cache_id][$op] = TRUE;
1549 }
1550
1551 // We grant access to the file if both of the following conditions are met:
1552 // - No modules say to deny access.
1553 // - At least one module says to grant access.
1554 $access = module_invoke_all('file_entity_access', $op, $file, $account);
1555 if (in_array(FILE_ENTITY_ACCESS_DENY, $access, TRUE)) {
1556 return $rights[$account->uid][$cache_id][$op] = FALSE;
1557 }
1558 elseif (in_array(FILE_ENTITY_ACCESS_ALLOW, $access, TRUE)) {
1559 return $rights[$account->uid][$cache_id][$op] = TRUE;
1560 }
1561
1562
1563 // Fall back to default behaviors on view.
1564 if ($op == 'view' && is_object($file)) {
1565 $scheme = file_uri_scheme($file->uri);
1566 $wrapper = file_entity_get_stream_wrapper($scheme);
1567
1568 if (!empty($wrapper['private'])) {
1569 // For private files, users can view private files if the
1570 // user is not anonymous, and has the 'view private files' permission.
1571 if (!empty($account->uid) && user_access('view private files', $account)) {
1572 return $rights[$account->uid][$cache_id][$op] = TRUE;
1573 }
1574
1575 // For private files, users can view their own private files if the
1576 // user is not anonymous, and has the 'view own private files' permission.
1577 if (!empty($account->uid) && $file->uid == $account->uid && user_access('view own private files', $account)) {
1578 return $rights[$account->uid][$cache_id][$op] = TRUE;
1579 }
1580 }
1581 elseif ($file->status == FILE_STATUS_PERMANENT && $file->uid == $account->uid && user_access('view own files', $account)) {
1582 // For non-private files, allow to see if user owns the file.
1583 return $rights[$account->uid][$cache_id][$op] = TRUE;
1584 }
1585 elseif ($file->status == FILE_STATUS_PERMANENT && user_access('view files', $account)) {
1586 // For non-private files, users can view if they have the 'view files'
1587 // permission.
1588 return $rights[$account->uid][$cache_id][$op] = TRUE;
1589 }
1590 }
1591
1592 return FALSE;
1593 }
1594
1595 /**
1596 * Implements hook_file_entity_access().
1597 */
1598 function file_entity_file_entity_access($op, $file, $account) {
1599 $grants = array();
1600
1601 // If the file URI is invalid, deny access.
1602 if (is_object($file) && !file_valid_uri($file->uri)) {
1603 return FILE_ENTITY_ACCESS_DENY;
1604 }
1605
1606 if ($op == 'create') {
1607 if (user_access('create files')) {
1608 return FILE_ENTITY_ACCESS_ALLOW;
1609 }
1610 }
1611
1612 if ($op == 'download') {
1613 if (user_access('download any files', $account) || (is_object($file) && user_access('download own files', $account) && ($account->uid == $file->uid))) {
1614 return FILE_ENTITY_ACCESS_ALLOW;
1615 }
1616 }
1617
1618 if ($op == 'update') {
1619 if (user_access('edit any files', $account) || (is_object($file) && user_access('edit own files', $account) && ($account->uid == $file->uid))) {
1620 return FILE_ENTITY_ACCESS_ALLOW;
1621 }
1622 }
1623
1624 if ($op == 'delete') {
1625 if (user_access('delete any files', $account) || (is_object($file) && user_access('delete own files', $account) && ($account->uid == $file->uid))) {
1626 return FILE_ENTITY_ACCESS_ALLOW;
1627 }
1628 }
1629
1630 if ($op == 'view' && is_object($file) && file_uri_scheme($file->uri) == 'private') {
1631 // When viewing private files, we can only invoke hook_file_download()
1632 // if the $account user objet matches the current user.
1633 if ($GLOBALS['user']->uid == $account->uid) {
1634 foreach (module_implements('file_download') as $module) {
1635 $access = module_invoke($module, 'file_download', $file->uri);
1636 if ($access === -1) {
1637 return FILE_ENTITY_ACCESS_DENY;
1638 }
1639 elseif (!empty($access)) {
1640 $grants[] = $access;
1641 }
1642 }
1643 }
1644 }
1645
1646 return !empty($grants) ? FILE_ENTITY_ACCESS_ALLOW : FILE_ENTITY_ACCESS_IGNORE;
1647 }
1648
1649 /**
1650 * @} End of "defgroup file_entity_access".
1651 *
1652 * Implements hook_file_default_types().
1653 */
1654 function file_entity_file_default_types() {
1655 $types = array();
1656
1657 // Image.
1658 $types['image'] = (object) array(
1659 'api_version' => 1,
1660 'type' => 'image',
1661 'label' => t('Image'),
1662 'description' => t('An <em>Image</em> file is a still visual.'),
1663 'mimetypes' => array(
1664 'image/jpeg',
1665 'image/gif',
1666 'image/png',
1667 ),
1668 'streams' => array(
1669 'public',
1670 ),
1671 );
1672
1673 // Video.
1674 $types['video'] = (object) array(
1675 'api_version' => 1,
1676 'type' => 'video',
1677 'label' => t('Video'),
1678 'description' => t('A <em>Video</em> file is a moving visual recording.'),
1679 'mimetypes' => array(
1680 'video/quicktime',
1681 'video/mp4',
1682 'video/x-msvideo',
1683 'video/ogg',
1684 'video/x-flv',
1685 ),
1686 'streams' => array(
1687 'public',
1688 ),
1689 );
1690
1691 // Audio.
1692 $types['audio'] = (object) array(
1693 'api_version' => 1,
1694 'type' => 'audio',
1695 'label' => t('Audio'),
1696 'description' => t('An <em>Audio</em> file is a sound recording.'),
1697 'mimetypes' => array(
1698 'audio/mpeg',
1699 'audio/x-ms-wma',
1700 'audio/x-wav',
1701 'audio/ogg',
1702 ),
1703 'streams' => array(
1704 'public',
1705 ),
1706 );
1707
1708 // Document.
1709 $types['document'] = (object) array(
1710 'api_version' => 1,
1711 'type' => 'document',
1712 'label' => t('Document'),
1713 'description' => t('A <em>Document</em> file is written information.'),
1714 'mimetypes' => array(
1715 'text/plain',
1716 'application/msword',
1717 'application/vnd.ms-excel',
1718 'application/pdf',
1719 'application/vnd.ms-powerpoint',
1720 'application/vnd.oasis.opendocument.text',
1721 'application/vnd.oasis.opendocument.spreadsheet',
1722 'application/vnd.oasis.opendocument.presentation',
1723 ),
1724 'streams' => array(
1725 'public',
1726 ),
1727 );
1728
1729 return $types;
1730 }
1731
1732 /**
1733 * Implements hook_file_operations().
1734 */
1735 function file_entity_file_operations() {
1736 $operations = array(
1737 'permanent' => array(
1738 'label' => t('Indicate that the selected files are permanent and should not be deleted'),
1739 'callback' => 'file_entity_mass_update',
1740 'callback arguments' => array('updates' => array('status' => FILE_STATUS_PERMANENT)),
1741 ),
1742 'temporary' => array(
1743 'label' => t('Indicate that the selected files are temporary and should be removed during cron runs'),
1744 'callback' => 'file_entity_mass_update',
1745 'callback arguments' => array('updates' => array('status' => 0)),
1746 ),
1747 'delete' => array(
1748 'label' => t('Delete selected files'),
1749 'callback' => NULL,
1750 ),
1751 );
1752 return $operations;
1753 }
1754
1755 /**
1756 * Clear the field cache for any entities referencing a specific file.
1757 *
1758 * @param object $file
1759 * A file object.
1760 */
1761 function file_entity_invalidate_field_caches($file) {
1762 $entity_types = &drupal_static(__FUNCTION__);
1763
1764 // Gather the list of entity types which support field caching.
1765 if (!isset($entity_types)) {
1766 $entity_types = array();
1767 foreach (entity_get_info() as $entity_type => $entity_info) {
1768 if (!empty($entity_info['fieldable']) && !empty($entity_info['field cache'])) {
1769 $entity_types[] = $entity_type;
1770 }
1771 }
1772 }
1773
1774 // If no entity types support field caching, then there is no work to be done.
1775 if (empty($entity_types)) {
1776 return;
1777 }
1778
1779 $records = db_query("SELECT DISTINCT type, id FROM {file_usage} WHERE fid = :fid AND type IN (:types) AND id > 0", array(':fid' => $file->fid, ':types' => $entity_types))->fetchAll();
1780 if (!empty($records)) {
1781 $cids = array();
1782 foreach ($records as $record) {
1783 $cids[] = 'field:' . $record->type . ':' . $record->id;
1784 }
1785 cache_clear_all($cids, 'cache_field');
1786 }
1787 }
1788
1789 /**
1790 * Check if a file entity is considered local or not.
1791 *
1792 * @param object $file
1793 * A file entity object from file_load().
1794 *
1795 * @return
1796 * TRUE if the file is using a local stream wrapper, or FALSE otherwise.
1797 */
1798 function file_entity_file_is_local($file) {
1799 $scheme = file_uri_scheme($file->uri);
1800 $wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL);
1801 return !empty($wrappers[$scheme]) && empty($wrappers[$scheme]['remote']);
1802 }
1803
1804 /**
1805 * Check if a file entity is considered writeable or not.
1806 *
1807 * @param object $file
1808 * A file entity object from file_load().
1809 *
1810 * @return
1811 * TRUE if the file is using a visible, readable and writeable stream wrapper,
1812 * or FALSE otherwise.
1813 */
1814 function file_entity_file_is_writeable($file) {
1815 $scheme = file_uri_scheme($file->uri);
1816 $wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE);
1817 return !empty($wrappers[$scheme]);
1818 }
1819
1820 /**
1821 * Pre-render callback for adding validation descriptions to file upload fields.
1822 */
1823 function file_entity_upload_validators_pre_render($element) {
1824 if (!empty($element['#upload_validators'])) {
1825 if (!isset($element['#description'])) {
1826 $element['#description'] = '';
1827 }
1828 if ($element['#description'] !== FALSE) {
1829 $element['#description'] = theme('file_upload_help', array('description' => $element['#description'], 'upload_validators' => $element['#upload_validators']));
1830 }
1831 }
1832 return $element;
1833 }
1834
1835 /**
1836 * Implements hook_form_FORM_ID_alter() for file_entity_edit() on behalf of path.module.
1837 */
1838 function path_form_file_entity_edit_alter(&$form, $form_state) {
1839 // Make sure this does not show up on the delete confirmation form.
1840 if (empty($form_state['confirm_delete'])) {
1841 $file = $form_state['file'];
1842 $langcode = function_exists('entity_language') ? entity_language('file', $file) : NULL;
1843 $langcode = !empty($langcode) ? $langcode : LANGUAGE_NONE;
1844 $conditions = array('source' => 'file/' . $file->fid, 'language' => $langcode);
1845 $path = (isset($file->fid) ? path_load($conditions) : array());
1846 if ($path === FALSE) {
1847 $path = array();
1848 }
1849 $path += array(
1850 'pid' => NULL,
1851 'source' => isset($file->fid) ? 'file/' . $file->fid : NULL,
1852 'alias' => '',
1853 'language' => $langcode,
1854 );
1855 $form['path'] = array(
1856 '#type' => 'fieldset',
1857 '#title' => t('URL path settings'),
1858 '#collapsible' => TRUE,
1859 '#collapsed' => empty($path['alias']),
1860 '#group' => 'additional_settings',
1861 '#attributes' => array(
1862 'class' => array('path-form'),
1863 ),
1864 '#attached' => array(
1865 'js' => array(drupal_get_path('module', 'path') . '/path.js'),
1866 ),
1867 '#access' => user_access('create url aliases') || user_access('administer url aliases'),
1868 '#weight' => 30,
1869 '#tree' => TRUE,
1870 '#element_validate' => array('path_form_element_validate'),
1871 );
1872 $form['path']['alias'] = array(
1873 '#type' => 'textfield',
1874 '#title' => t('URL alias'),
1875 '#default_value' => $path['alias'],
1876 '#maxlength' => 255,
1877 '#description' => t('Optionally specify an alternative URL by which this file can be accessed. For example, type "about" when writing an about page. Use a relative path and don\'t add a trailing slash or the URL alias won\'t work.'),
1878 );
1879 $form['path']['pid'] = array('#type' => 'value', '#value' => $path['pid']);
1880 $form['path']['source'] = array('#type' => 'value', '#value' => $path['source']);
1881 $form['path']['language'] = array('#type' => 'value', '#value' => $path['language']);
1882 }
1883 }
1884
1885 /**
1886 * Implements hook_file_insert() on behalf of path.module.
1887 */
1888 function path_file_insert($file) {
1889 if (isset($file->path)) {
1890 $path = $file->path;
1891 $path['alias'] = trim($path['alias']);
1892 // Only save a non-empty alias.
1893 if (!empty($path['alias'])) {
1894 // Ensure fields for programmatic executions.
1895 $path['source'] = 'file/' . $file->tid;
1896 // Core does not provide a way to store the file language but contrib
1897 // modules can do it so we need to take this into account.
1898 $langcode = entity_language('file', $file);
1899 $path['language'] = !empty($langcode) ? $langcode : LANGUAGE_NONE;
1900 path_save($path);
1901 }
1902 }
1903 }
1904
1905 /**
1906 * Implements hook_file_update() on behalf of path.module.
1907 */
1908 function path_file_update($file) {
1909 if (isset($file->path)) {
1910 $path = $file->path;
1911 $path['alias'] = trim($path['alias']);
1912 // Delete old alias if user erased it.
1913 if (!empty($path['fid']) && empty($path['alias'])) {
1914 path_delete($path['fid']);
1915 }
1916 // Only save a non-empty alias.
1917 if (!empty($path['alias'])) {
1918 // Ensure fields for programmatic executions.
1919 $path['source'] = 'file/' . $file->fid;
1920 // Core does not provide a way to store the file language but contrib
1921 // modules can do it so we need to take this into account.
1922 $langcode = entity_language('file', $file);
1923 $path['language'] = !empty($langcode) ? $langcode : LANGUAGE_NONE;
1924 path_save($path);
1925 }
1926 }
1927 }
1928
1929 /**
1930 * Implements hook_file_delete() on behalf of path.module.
1931 */
1932 function path_file_delete($file) {
1933 // Delete all aliases associated with this file.
1934 path_delete(array('source' => 'file/' . $file->fid));
1935 }