Issue #1881152 by arthurf, rootatwc: Browser display cleanup
[project/media.git] / includes / media.filter.inc
1 <?php
2
3 /**
4 * @file
5 * Functions related to the WYSIWYG editor and the media input filter.
6 *
7 * @TODO: Rename this file?
8 */
9
10 define('MEDIA_TOKEN_REGEX', '/\[\[.*?\]\]/s');
11 define('MEDIA_TOKEN_REGEX_ALT', '/%7B.*?%7D/s');
12
13 /**
14 * Implements hook_wysiwyg_include_directory().
15 */
16 function media_wysiwyg_include_directory($type) {
17 switch ($type) {
18 case 'plugins':
19 return 'wysiwyg_plugins';
20
21 break;
22 }
23 }
24
25 /**
26 * Implements hook_field_attach_insert().
27 *
28 * Track file usage for media files included in formatted text. Note that this
29 * is heavy-handed, and should be replaced when Drupal's filter system is
30 * context-aware.
31 */
32 function media_field_attach_insert($entity_type, $entity) {
33 _media_filter_add_file_usage_from_fields($entity_type, $entity);
34 }
35
36 /**
37 * Implements hook_field_attach_update().
38 *
39 * @see media_field_attach_insert().
40 */
41 function media_field_attach_update($entity_type, $entity) {
42 _media_filter_add_file_usage_from_fields($entity_type, $entity);
43 }
44
45 /**
46 * Add file usage from file references in an entity's text fields.
47 */
48 function _media_filter_add_file_usage_from_fields($entity_type, $entity) {
49 // Track the total usage for files from all fields combined.
50 $entity_files = media_entity_field_count_files($entity_type, $entity);
51
52 list($entity_id, $entity_vid, $entity_bundle) = entity_extract_ids($entity_type, $entity);
53
54 // When an entity has revisions and then is saved again NOT as new version the
55 // previous revision of the entity has be loaded to get the last known good
56 // count of files. The saved data is compared against the last version
57 // so that a correct file count can be created for that (the current) version
58 // id. This code may assume some things about entities that are only true for
59 // node objects. This should be reviewed.
60 // @TODO this conditional can probably be condensed
61 if (empty($entity->revision) && empty($entity->old_vid) && empty($entity->is_new) && ! empty($entity->original)) {
62 $old_files = media_entity_field_count_files($entity_type, $entity->original);
63 foreach ($old_files as $fid => $old_file_count) {
64 // Were there more files on the node just prior to saving?
65 if (empty($entity_files[$fid])) {
66 $entity_files[$fid] = 0;
67 }
68 if ($old_file_count > $entity_files[$fid]) {
69 $deprecate = $old_file_count - $entity_files[$fid];
70 // Now deprecate this usage
71 $file = file_load($fid);
72 file_usage_delete($file, 'media', $entity_type, $entity_id, $deprecate);
73 // Usage is deleted, nothing more to do with this file
74 unset($entity_files[$fid]);
75 }
76 // There are the same number of files, nothing to do
77 elseif ($entity_files[$fid] == $old_file_count) {
78 unset($entity_files[$fid]);
79 }
80 // There are more files now, adjust the difference for the greater number.
81 // file_usage incrementing will happen below.
82 else {
83 // We just need to adjust what the file count will account for the new
84 // images that have been added since the increment process below will
85 // just add these additional ones in
86 $entity_files[$fid] = $entity_files[$fid] - $old_file_count;
87 }
88 }
89 }
90
91 // Each entity revision counts for file usage. If versions are not enabled
92 // the file_usage table will have no entries for this because of the delete
93 // query above.
94 foreach ($entity_files as $fid => $entity_count) {
95 $file = file_load($fid);
96 file_usage_add($file, 'media', $entity_type, $entity_id, $entity_count);
97 }
98
99 }
100
101 /**
102 * Parse file references from an entity's text fields and return them as an array.
103 */
104 function media_filter_parse_from_fields($entity_type, $entity) {
105 $file_references = array();
106
107 foreach (_media_filter_fields_with_text_filtering($entity_type, $entity) as $field_name) {
108 if ($field_items = field_get_items($entity_type, $entity, $field_name)) {
109 foreach ($field_items as $field_item) {
110 preg_match_all(MEDIA_TOKEN_REGEX, $field_item['value'], $matches);
111 foreach ($matches[0] as $tag) {
112 $tag = str_replace(array('[[', ']]'), '', $tag);
113 $tag_info = drupal_json_decode($tag);
114 if (isset($tag_info['fid']) && $tag_info['type'] == 'media') {
115 $file_references[] = $tag_info;
116 }
117 }
118
119 preg_match_all(MEDIA_TOKEN_REGEX_ALT, $field_item['value'], $matches_alt);
120 foreach ($matches_alt[0] as $tag) {
121 $tag = urldecode($tag);
122 $tag_info = drupal_json_decode($tag);
123 if (isset($tag_info['fid']) && $tag_info['type'] == 'media') {
124 $file_references[] = $tag_info;
125 }
126 }
127 }
128 }
129 }
130
131 return $file_references;
132 }
133
134 /**
135 * Returns an array containing the names of all fields that perform text filtering.
136 */
137 function _media_filter_fields_with_text_filtering($entity_type, $entity) {
138 list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
139 $fields = field_info_instances($entity_type, $bundle);
140
141 // Get all of the fields on this entity that allow text filtering.
142 $fields_with_text_filtering = array();
143 foreach ($fields as $field_name => $field) {
144 if (!empty($field['settings']['text_processing'])) {
145 $fields_with_text_filtering[] = $field_name;
146 }
147 }
148
149 return $fields_with_text_filtering;
150 }
151
152 /**
153 * Utility function to get the file count in this entity
154 *
155 * @param type $entity
156 * @param type $entity_type
157 * @return int
158 */
159 function media_entity_field_count_files($entity_type, $entity) {
160 $entity_files = array();
161 foreach (media_filter_parse_from_fields($entity_type, $entity) as $file_reference) {
162 if (empty($entity_files[$file_reference['fid']])) {
163 $entity_files[$file_reference['fid']] = 1;
164 }
165 else {
166 $entity_files[$file_reference['fid']]++;
167 }
168 }
169 return $entity_files;
170 }
171
172 /**
173 * Implements hook_entity_delete().
174 */
175 function media_entity_delete($entity, $type) {
176 list($entity_id) = entity_extract_ids($type, $entity);
177
178 db_delete('file_usage')
179 ->condition('module', 'media')
180 ->condition('type', $type)
181 ->condition('id', $entity_id)
182 ->execute();
183 }
184
185 /**
186 * Implements hook_field_attach_delete_revision().
187 *
188 * @param type $entity_type
189 * @param type $entity
190 */
191 function media_field_attach_delete_revision($entity_type, $entity) {
192 list($entity_id) = entity_extract_ids($entity_type, $entity);
193 $files = media_entity_field_count_files($entity_type, $entity);
194 foreach ($files as $fid => $count) {
195 if ($file = file_load($fid)) {
196 file_usage_delete($file, 'media', $entity_type , $entity_id, $count);
197 }
198 }
199 }
200
201 /**
202 * Filter callback for media markup filter.
203 *
204 * @TODO check for security probably pass text through filter_xss
205 */
206 function media_filter($text) {
207 $text = preg_replace_callback(MEDIA_TOKEN_REGEX, 'media_token_to_markup', $text);
208 return $text;
209 }
210
211 /**
212 * Parses the contents of a CSS declaration block.
213 *
214 * @param string $declarations
215 * One or more CSS declarations delimited by a semicolon. The same as a CSS
216 * declaration block (see http://www.w3.org/TR/CSS21/syndata.html#rule-sets),
217 * but without the opening and closing curly braces. Also the same as the
218 * value of an inline HTML style attribute.
219 *
220 * @return array
221 * A keyed array. The keys are CSS property names, and the values are CSS
222 * property values.
223 */
224 function media_parse_css_declarations($declarations) {
225 $properties = array();
226 foreach (array_map('trim', explode(";", $declarations)) as $declaration) {
227 if ($declaration != '') {
228 list($name, $value) = array_map('trim', explode(':', $declaration, 2));
229 $properties[strtolower($name)] = $value;
230 }
231 }
232 return $properties;
233 }
234
235 /**
236 * Replace callback to convert a media file tag into HTML markup.
237 *
238 * @param string $match
239 * Takes a match of tag code
240 * @param bool $wysiwyg
241 * Set to TRUE if called from within the WYSIWYG text area editor.
242 *
243 * @return string
244 * The HTML markup representation of the tag, or an empty string on failure.
245 *
246 * @see media_get_file_without_label()
247 * @see hook_media_token_to_markup_alter()
248 */
249 function media_token_to_markup($match, $wysiwyg = FALSE) {
250 $settings = array();
251 $match = str_replace("[[", "", $match);
252 $match = str_replace("]]", "", $match);
253 $tag = $match[0];
254
255 try {
256 if (!is_string($tag)) {
257 throw new Exception('Unable to find matching tag');
258 }
259
260 $tag_info = drupal_json_decode($tag);
261
262 if (!isset($tag_info['fid'])) {
263 throw new Exception('No file Id');
264 }
265
266 // Ensure a valid view mode is being requested.
267 if (!isset($tag_info['view_mode'])) {
268 $tag_info['view_mode'] = media_variable_get('wysiwyg_default_view_mode');
269 }
270 elseif ($tag_info['view_mode'] != 'default') {
271 $file_entity_info = entity_get_info('file');
272 if (!in_array($tag_info['view_mode'], array_keys($file_entity_info['view modes']))) {
273 // Media 1.x defined some old view modes that have been superseded by
274 // more semantically named ones in File Entity. The media_update_7203()
275 // function updates field settings that reference the old view modes,
276 // but it's impractical to update all text content, so adjust
277 // accordingly here.
278 static $view_mode_updates = array(
279 'media_preview' => 'preview',
280 'media_small' => 'teaser',
281 'media_large' => 'full',
282 );
283 if (isset($view_mode_updates[$tag_info['view_mode']])) {
284 $tag_info['view_mode'] = $view_mode_updates[$tag_info['view_mode']];
285 }
286 else {
287 throw new Exception('Invalid view mode');
288 }
289 }
290 }
291
292 $file = file_load($tag_info['fid']);
293 if (!$file) {
294 throw new Exception('Could not load media object');
295 }
296 $tag_info['file'] = $file;
297
298 // The class attributes is a string, but drupal requires it to be
299 // an array, so we fix it here.
300 if (!empty($tag_info['attributes']['class'])) {
301 $tag_info['attributes']['class'] = explode(" ", $tag_info['attributes']['class']);
302 }
303
304 // Track the fid of this file in the {media_filter_usage} table.
305 media_filter_track_usage($file->fid);
306
307 $attributes = is_array($tag_info['attributes']) ? $tag_info['attributes'] : array();
308 $attribute_whitelist = media_variable_get('wysiwyg_allowed_attributes');
309 $settings['attributes'] = array_intersect_key($attributes, array_flip($attribute_whitelist));
310
311 if (!empty($tag_info['attributes']) && is_array($tag_info['attributes'])) {
312 $attribute_whitelist = media_variable_get('wysiwyg_allowed_attributes');
313 $settings['attributes'] = array_intersect_key($tag_info['attributes'], array_flip($attribute_whitelist));
314
315 // Many media formatters will want to apply width and height independently
316 // of the style attribute or the corresponding HTML attributes, so pull
317 // these two out into top-level settings. Different WYSIWYG editors have
318 // different behavior with respect to whether they store user-specified
319 // dimensions in the HTML attributes or the style attribute - check both.
320 // Per http://www.w3.org/TR/html5/the-map-element.html#attr-dim-width, the
321 // HTML attributes are merely hints: CSS takes precedence.
322 if (isset($settings['attributes']['style'])) {
323 $css_properties = media_parse_css_declarations($settings['attributes']['style']);
324 foreach (array('width', 'height') as $dimension) {
325 if (isset($css_properties[$dimension]) && substr($css_properties[$dimension], -2) == 'px') {
326 $settings[$dimension] = substr($css_properties[$dimension], 0, -2);
327 }
328 elseif (isset($settings['attributes'][$dimension])) {
329 $settings[$dimension] = $settings['attributes'][$dimension];
330 }
331 }
332 }
333 }
334 }
335 catch (Exception $e) {
336 watchdog('media', 'Unable to render media from %tag. Error: %error', array('%tag' => $tag, '%error' => $e->getMessage()));
337 return '';
338 }
339
340 if ($wysiwyg) {
341 $settings['wysiwyg'] = $wysiwyg;
342 // If sending markup to a WYSIWYG, we need to pass the file infomation so
343 // that a inline macro can be generated when the WYSIWYG is detached.
344 // The WYSIWYG plugin is expecting this information in the format of a
345 // urlencoded JSON string stored in the data-file_info attribute of the
346 // element.
347 $element = media_get_file_without_label($file, $tag_info['view_mode'], $settings);
348 $data = drupal_json_encode(array(
349 'type' => 'media',
350 'fid' => $file->fid,
351 'view_mode' => $tag_info['view_mode'],
352 ));
353 $element['#attributes']['data-file_info'] = urlencode($data);
354 $element['#attributes']['class'][] = 'media-element';
355 }
356 else {
357 // Display the field elements.
358 $element = array();
359 $element['content']['file'] = media_get_file_without_label($file, $tag_info['view_mode'], $settings);
360 // Overwrite or set the file #alt attribute if it has been set in this
361 // instance.
362 if (!empty($element['content']['file']['#attributes']['alt'])) {
363 $element['content']['file']['#alt'] = $element['content']['file']['#attributes']['alt'];
364 }
365 // Overwrite or set the file #title attribute if it has been set in this
366 // instance.
367 if (!empty($element['content']['file']['#attributes']['title'])) {
368 $element['content']['file']['#title'] = $element['content']['file']['#attributes']['title'];
369 }
370 field_attach_prepare_view('file', array($file->fid => $file), $tag_info['view_mode']);
371 entity_prepare_view('file', array($file->fid => $file));
372 $element['content'] += field_attach_view('file', $file, $tag_info['view_mode']);
373 if (count(element_children($element['content'])) > 1) {
374 // Add surrounding divs to group them together.
375 // We dont want divs when there are no additional fields to allow files
376 // to display inline with text, without breaking p tags.
377 $element['content']['#type'] = 'container';
378 $element['content']['#attributes']['class'] = array(
379 'media',
380 'media-element-container',
381 'media-' . $element['content']['file']['#view_mode']
382 );
383 }
384 }
385 drupal_alter('media_token_to_markup', $element, $tag_info, $settings);
386 return drupal_render($element);
387 }
388
389 /**
390 * Builds a map of media tags in the element.
391 *
392 * Builds a map of the media tags in an element that are being rendered to their
393 * rendered HTML. The map is stored in JS, so we can transform them when the
394 * editor is being displayed.
395 */
396 function media_pre_render_text_format($element) {
397 // filter_process_format() copies properties to the expanded 'value' child
398 // element.
399 if (!isset($element['format'])) {
400 return $element;
401 }
402
403 $field = &$element['value'];
404 $settings = array(
405 'field' => $field['#id'],
406 );
407
408 $tagmap = _media_generate_tagMap($field['#value']);
409
410 if (isset($tagmap)) {
411 drupal_add_js(array('tagmap' => $tagmap), 'setting');
412 }
413 return $element;
414 }
415
416 /**
417 * Creates map of inline media tags.
418 *
419 * Generates an array of [inline tags] => <html> to be used in filter
420 * replacement and to add the mapping to JS.
421 *
422 * @param string $text
423 * The String containing text and html markup of textarea
424 *
425 * @return array
426 * An associative array with tag code as key and html markup as the value.
427 *
428 * @see media_process_form()
429 * @see media_token_to_markup()
430 */
431 function _media_generate_tagMap($text) {
432 // Making $tagmap static as this function is called many times and
433 // adds duplicate markup for each tag code in Drupal.settings JS,
434 // so in media_process_form it adds something like tagCode:<markup>,
435 // <markup> and when we replace in attach see two duplicate images
436 // for one tagCode. Making static would make function remember value
437 // between function calls. Since media_process_form is multiple times
438 // with same form, this function is also called multiple times.
439 static $tagmap = array();
440 preg_match_all("/\[\[.*?\]\]/s", $text, $matches, PREG_SET_ORDER);
441 foreach ($matches as $match) {
442 // We see if tagContent is already in $tagMap, if not we add it
443 // to $tagmap. If we return an empty array, we break embeddings of the same
444 // media multiple times.
445 if (empty($tagmap[$match[0]])) {
446 // @TODO: Total HACK, but better than nothing.
447 // We should find a better way of cleaning this up.
448 if ($markup_for_media = media_token_to_markup($match, TRUE)) {
449 $tagmap[$match[0]] = $markup_for_media;
450 }
451 else {
452 $missing = file_create_url(drupal_get_path('module', 'media') . '/images/icons/default/image-x-generic.png');
453 $tagmap[$match[0]] = '<div><img src="' . $missing . '" width="100px" height="100px"/></div>';
454 }
455 }
456 }
457 return $tagmap;
458 }
459
460 /**
461 * Return a list of view modes allowed for a file embedded in the WYSIWYG.
462 *
463 * @param object $file
464 * A file entity.
465 *
466 * @return array
467 * An array of view modes that can be used on the file when embedded in the
468 * WYSIWYG.
469 */
470 function media_get_wysiwyg_allowed_view_modes($file) {
471 $enabled_view_modes = &drupal_static(__FUNCTION__, array());
472
473 // @todo Add more caching for this.
474 if (!isset($enabled_view_modes[$file->type])) {
475 $enabled_view_modes[$file->type] = array();
476
477 // Add the default view mode by default.
478 $enabled_view_modes[$file->type]['default'] = array('label' => t('Default'), 'custom settings' => TRUE);
479
480 $entity_info = entity_get_info('file');
481 $view_mode_settings = field_view_mode_settings('file', $file->type);
482 foreach ($entity_info['view modes'] as $view_mode => $view_mode_info) {
483 // Do not show view modes that don't have their own settings and will
484 // only fall back to the default view mode.
485 if (empty($view_mode_settings[$view_mode]['custom_settings'])) {
486 continue;
487 }
488
489 // Don't present the user with an option to choose a view mode in which
490 // the file is hidden.
491 $extra_fields = field_extra_fields_get_display('file', $file->type, $view_mode);
492 if (empty($extra_fields['file']['visible'])) {
493 continue;
494 }
495
496 // Add the view mode to the list of enabled view modes.
497 $enabled_view_modes[$file->type][$view_mode] = $view_mode_info;
498 }
499 }
500
501 $view_modes = $enabled_view_modes[$file->type];
502 drupal_alter('media_wysiwyg_allowed_view_modes', $view_modes, $file);
503 return $view_modes;
504 }
505
506 /**
507 * Form callback used when embedding media.
508 *
509 * Allows the user to pick a format for their media file.
510 * Can also have additional params depending on the media type.
511 */
512 function media_format_form($form, $form_state, $file) {
513 $form = array();
514 $form['#media'] = $file;
515
516 $view_modes = media_get_wysiwyg_allowed_view_modes($file);
517 $formats = $options = array();
518 foreach ($view_modes as $view_mode => $view_mode_info) {
519 // @TODO: Display more verbose information about which formatter and what it
520 // does.
521 $options[$view_mode] = $view_mode_info['label'];
522 $element = media_get_file_without_label($file, $view_mode, array('wysiwyg' => TRUE));
523
524 // Make a pretty name out of this.
525 $formats[$view_mode] = drupal_render($element);
526 }
527
528 // Add the previews back into the form array so they can be altered.
529 $form['#formats'] = &$formats;
530
531 if (!count($formats)) {
532 throw new Exception('Unable to continue, no available formats for displaying media.');
533 return;
534 }
535
536 $default_view_mode = media_variable_get('wysiwyg_default_view_mode');
537 if (!isset($formats[$default_view_mode])) {
538 $default_view_mode = key($formats);
539 }
540
541 // Add the previews by reference so that they can easily be altered by
542 // changing $form['#formats'].
543 $settings['media']['formatFormFormats'] = &$formats;
544 $form['#attached']['js'][] = array('data' => $settings, 'type' => 'setting');
545
546 // Add the required libraries, JavaScript and CSS for the form.
547 $form['#attached']['library'][] = array('media', 'media_base');
548 $form['#attached']['library'][] = array('system', 'form');
549 $form['#attached']['js'][] = drupal_get_path('module', 'media') . '/js/media.format_form.js';
550
551 $form['title'] = array(
552 '#markup' => t('Embedding %filename', array('%filename' => $file->filename)),
553 );
554
555 $preview = media_get_thumbnail_preview($file);
556
557 $form['preview'] = array(
558 '#type' => 'markup',
559 '#title' => check_plain(basename($file->uri)),
560 '#markup' => drupal_render($preview),
561 );
562
563 // These will get passed on to WYSIWYG.
564 $form['options'] = array(
565 '#type' => 'fieldset',
566 '#title' => t('options'),
567 );
568
569 $form['options']['format'] = array(
570 '#type' => 'select',
571 '#title' => t('Display as'),
572 '#options' => $options,
573 '#default_value' => $default_view_mode,
574 '#description' => t('Choose the type of display you would like for this
575 file. Please be aware that files may display differently than they do when
576 they are insterted into an editor.')
577 );
578
579 if ($file->type === 'image') {
580 $form['options']['alt'] = array(
581 '#type' => 'textfield',
582 '#title' => t('Alternate text'),
583 '#description' => t('Alternative text is used by screen readers, search
584 engines, and when the image cannot be loaded. By adding alt text you
585 improve accesibility and search engine optimization.'),
586 '#default_value' => isset($file->field_file_image_alt_text['und'][0]['safe_value']) ? $file->field_file_image_alt_text['und'][0]['safe_value'] : '',
587 );
588 $form['options']['title'] = array(
589 '#type' => 'textfield',
590 '#title' => t('Title text'),
591 '#description' => t('Title text is used in the tool tip when a user hovers
592 their mouse over the image. Adding title text makes it easier to
593 understand the context of an image and improves usability.'),
594 '#default_value' => isset($file->field_file_image_title_text['und'][0]['safe_value']) ? $file->field_file_image_title_text['und'][0]['safe_value'] : '',
595 );
596 }
597
598 // Similar to a form_alter, but we want this to run first so that
599 // media.types.inc can add the fields specific to a given type (like alt tags
600 // on media). If implemented as an alter, this might not happen, making other
601 // alters not be able to work on those fields.
602 // @todo: We need to pass in existing values for those attributes.
603 drupal_alter('media_format_form_prepare', $form, $form_state, $file);
604
605 if (!element_children($form['options'])) {
606 $form['options']['#attributes'] = array('style' => 'display:none');
607 }
608
609 return $form;
610 }
611
612 /**
613 * Returns a drupal_render() array for just the file portion of a file entity.
614 *
615 * Optional custom settings can override how the file is displayed.
616 */
617 function media_get_file_without_label($file, $view_mode, $settings = array()) {
618 $file->override = $settings;
619
620 $element = file_view_file($file, $view_mode);
621
622 // The formatter invoked by file_view_file() can use $file->override to
623 // customize the returned render array to match the requested settings. To
624 // support simple formatters that don't do this, set the element attributes to
625 // what was requested, but not if the formatter applied its own logic for
626 // element attributes.
627 if (!isset($element['#attributes']) && isset($settings['attributes'])) {
628 $element['#attributes'] = $settings['attributes'];
629
630 // While this function may be called for any file type, images are a common
631 // use-case. theme_image() and theme_image_style() require the 'alt'
632 // attribute to be passed separately from the 'attributes' array (see
633 // http://drupal.org/node/999338). Until that's fixed, implement this
634 // special-case logic. Image formatters using other theme functions are
635 // responsible for their own 'alt' attribute handling. See
636 // theme_media_formatter_large_icon() for an example.
637 if (isset($settings['attributes']['alt']) && !isset($element['#alt']) && isset($element['#theme']) && in_array($element['#theme'], array('image', 'image_style'))) {
638 $element['#alt'] = $settings['attributes']['alt'];
639 }
640 }
641
642 return $element;
643 }
644
645 /**
646 * Clears caches that may be affected by the media filter.
647 *
648 * The media filter calls file_load(). This means that if a file object
649 * is updated, the check_markup() and field caches could return stale content.
650 * There are several possible approaches to deal with this:
651 * - Disable filter caching in media_filter_info(), this was found to cause a
652 * 30% performance hit from profiling four node teasers, due to both the
653 * media filter itself, and other filters that can't be cached.
654 * - Clear the filter and field caches whenever any media node is updated, this
655 * would ensure cache coherency but would reduce the effectiveness of those
656 * caches on high traffic sites with lots of media content updates.
657 * - The approach taken here: Record the fid of all media objects that are
658 * referenced by the media filter. Only clear the filter and field caches
659 * when one of these is updated, as opposed to all media objects.
660 * - @todo: consider an EntityFieldQuery to limit cache clearing to only those
661 * entities that use a text format with the media filter, possibly checking
662 * the contents of those fields to further limit this to fields referencing
663 * the media object being updated. This would need to be implemented
664 * carefully to avoid scalability issues with large result sets, and may
665 * not be worth the effort.
666 *
667 * @param int $fid
668 * Optional media fid being updated. If not given, the cache will be cleared
669 * as long as any file is referenced.
670 */
671 function media_filter_invalidate_caches($fid = FALSE) {
672 // If fid is passed, confirm that it has previously been referenced by the
673 // media filter. If not, clear the cache if the {media_filter_usage} has any
674 // valid records.
675 if (($fid && db_query('SELECT fid FROM {media_filter_usage} WHERE fid = :fid', array(':fid' => $fid))->fetchField()) || (!$fid && media_filter_usage_has_records())) {
676 // @todo: support entity cache, either via a hook, or using module_exists().
677 cache_clear_all('*', 'cache_filter', TRUE);
678 cache_clear_all('*', 'cache_field', TRUE);
679 }
680 }
681
682 /**
683 * Determines if the {media_filter_usage} table has any entries.
684 */
685 function media_filter_usage_has_records() {
686 return (bool) db_query_range('SELECT 1 FROM {media_filter_usage} WHERE fid > :fid', 0, 1, array(':fid' => 0))->fetchField();
687 }
688
689 /**
690 * Tracks usage of media fids by the media filter.
691 *
692 * @param int $fid
693 * The media fid.
694 */
695 function media_filter_track_usage($fid) {
696 // This function only tracks when fids are found by the media filter.
697 // It would be impractical to check when formatted text is edited to remove
698 // references to fids, however by keeping a timestamp, we can implement
699 // rudimentary garbage collection in hook_flush_caches().
700 // However we only need to track that an fid has ever been referenced,
701 // not every time, so avoid updating this table more than once per month,
702 // per fid.
703 $timestamp = db_query('SELECT timestamp FROM {media_filter_usage} WHERE fid = :fid', array(':fid' => $fid))->fetchField();
704 if (!$timestamp || $timestamp <= REQUEST_TIME - 86400 * 30) {
705 db_merge('media_filter_usage')->key(array('fid' => $fid))->fields(array('fid' => $fid, 'timestamp' => REQUEST_TIME))->execute();
706 }
707 }