Newer
Older
<?php
/**
* @file
* Functions related to the WYSIWYG editor and the media input filter.
*
* @TODO: Rename this file?
*/
/**
* Implements hook_wysiwyg_include_directory().
*/
function media_wysiwyg_include_directory($type) {
case 'plugins':
return 'wysiwyg_plugins';
break;
}
}
/**
* Filter callback for media markup filter.
*
* @TODO check for security probably pass text through filter_xss
* @return unknown_type
*/
function media_filter($text) {
$text = ' ' . $text . ' ';
$text = preg_replace_callback("/\[\[.*?\]\]/s", 'media_token_to_markup', $text);
return $text;
}
Alex Bronstein
committed
/**
* Parses the contents of a CSS declaration block and returns a keyed array of property names and values.
*
* @param $declarations
* One or more CSS declarations delimited by a semicolon. The same as a CSS
* declaration block (see http://www.w3.org/TR/CSS21/syndata.html#rule-sets),
* but without the opening and closing curly braces. Also the same as the
* value of an inline HTML style attribute.
*
* @return
* A keyed array. The keys are CSS property names, and the values are CSS
* property values.
*/
function media_parse_css_declarations($declarations) {
$properties = array();
foreach (array_map('trim', explode(";", $declarations)) as $declaration) {
Alex Bronstein
committed
if ($declaration != '') {
list($name, $value) = array_map('trim', explode(':', $declaration, 2));
$properties[strtolower($name)] = $value;
}
}
return $properties;
}
* Replace callback to convert a media file tag into HTML markup.
*
* @param string $match
* Takes a match of tag code
* @param boolean $wysiwyg
* Set to TRUE if called from within the WYSIWYG text area editor.
* @return
* The HTML markup representation of the tag, or an empty string on failure.
*
* @see media_get_file_without_label()
* @see hook_media_token_to_markup_alter()
function media_token_to_markup($match, $wysiwyg = FALSE) {
Alex Bronstein
committed
$settings = array();
$match = str_replace("[[", "", $match);
$match = str_replace("]]", "", $match);
$tag = $match[0];
try {
if (!is_string($tag)) {
throw new Exception('Unable to find matching tag');
}
Alex Bronstein
committed
$tag_info = drupal_json_decode($tag);
Alex Bronstein
committed
if (!isset($tag_info['fid'])) {
throw new Exception('No file Id');
}
Paris Liakos
committed
// Ensure a valid view mode is being requested.
Alex Bronstein
committed
if (!isset($tag_info['view_mode'])) {
$tag_info['view_mode'] = media_variable_get('wysiwyg_default_view_mode');
Paris Liakos
committed
elseif ($tag_info['view_mode'] != 'default') {
$file_entity_info = entity_get_info('file');
if (!in_array($tag_info['view_mode'], array_keys($file_entity_info['view modes']))) {
// Media 1.x defined some old view modes that have been superseded by
// more semantically named ones in File Entity. The media_update_7203()
// function updates field settings that reference the old view modes,
// but it's impractical to update all text content, so adjust
// accordingly here.
static $view_mode_updates = array(
'media_preview' => 'preview',
'media_small' => 'teaser',
'media_large' => 'full',
);
if (isset($view_mode_updates[$tag_info['view_mode']])) {
$tag_info['view_mode'] = $view_mode_updates[$tag_info['view_mode']];
}
else {
throw new Exception('Invalid view mode');
}
}
}
Alex Bronstein
committed
$file = file_load($tag_info['fid']);
if (!$file) {
throw new Exception('Could not load media object');
$tag_info['file'] = $file;
Alex Bronstein
committed
Jimmy Axenhus
committed
// The class attributes is a string, but drupal requires it to be
// an array, so we fix it here.
if (!empty($tag_info['attributes']['class'])) {
$tag_info['attributes']['class'] = explode(" ", $tag_info['attributes']['class']);
}
Alex Bronstein
committed
// Track the fid of this file in the {media_filter_usage} table.
media_filter_track_usage($file->fid);
Alex Bronstein
committed
Alex Bronstein
committed
$attributes = is_array($tag_info['attributes']) ? $tag_info['attributes'] : array();
$attribute_whitelist = media_variable_get('wysiwyg_allowed_attributes');
Alex Bronstein
committed
$settings['attributes'] = array_intersect_key($attributes, array_flip($attribute_whitelist));
Paris Liakos
committed
if (!empty($tag_info['attributes']) && is_array($tag_info['attributes'])) {
$attribute_whitelist = media_variable_get('wysiwyg_allowed_attributes');
$settings['attributes'] = array_intersect_key($tag_info['attributes'], array_flip($attribute_whitelist));
// Many media formatters will want to apply width and height independently
// of the style attribute or the corresponding HTML attributes, so pull
// these two out into top-level settings. Different WYSIWYG editors have
// different behavior with respect to whether they store user-specified
// dimensions in the HTML attributes or the style attribute, so check both.
// Per http://www.w3.org/TR/html5/the-map-element.html#attr-dim-width, the
// HTML attributes are merely hints: CSS takes precedence.
if (isset($settings['attributes']['style'])) {
$css_properties = media_parse_css_declarations($settings['attributes']['style']);
foreach (array('width', 'height') as $dimension) {
if (isset($css_properties[$dimension]) && substr($css_properties[$dimension], -2) == 'px') {
$settings[$dimension] = substr($css_properties[$dimension], 0, -2);
}
elseif (isset($settings['attributes'][$dimension])) {
$settings[$dimension] = $settings['attributes'][$dimension];
}
Alex Bronstein
committed
}
}
}
}
catch (Exception $e) {
watchdog('media', 'Unable to render media from %tag. Error: %error', array('%tag' => $tag, '%error' => $e->getMessage()));
return '';
}
Paris Liakos
committed
$settings['wysiwyg'] = $wysiwyg;
// If sending markup to a WYSIWYG, we need to pass the file infomation so
// that a inline macro can be generated when the WYSIWYG is detached.
// The WYSIWYG plugin is expecting this information in the format of a
// urlencoded JSON string stored in the data-file_info attribute of the
// element.
$element = media_get_file_without_label($file, $tag_info['view_mode'], $settings);
Paris Liakos
committed
$data = drupal_json_encode(array(
'type' => 'media',
'fid' => $file->fid,
'view_mode' => $tag_info['view_mode'],
));
$element['#attributes']['data-file_info'] = urlencode($data);
$element['#attributes']['class'][] = 'media-element';
}
else {
// Display the field elements.
$element = array();
$element['content']['file'] = media_get_file_without_label($file, $tag_info['view_mode'], $settings);
// Overwrite or set the file #alt attribute if it has been set in this instance.
if (!empty($element['content']['file']['#attributes']['alt'])) {
$element['content']['file']['#alt'] = $element['content']['file']['#attributes']['alt'];
}
// Overwrite or set the file #title attribute if it has been set in this instance.
if (!empty($element['content']['file']['#attributes']['title'])) {
$element['content']['file']['#title'] = $element['content']['file']['#attributes']['title'];
}
field_attach_prepare_view('file', array($file->fid => $file), $tag_info['view_mode']);
entity_prepare_view('file', array($file->fid => $file));
$element['content'] += field_attach_view('file', $file, $tag_info['view_mode']);
Paris Liakos
committed
if (count(element_children($element['content'])) > 1) {
// Add surrounding divs to group them together.
// We dont want divs when there are no additional fields to allow them
// to display inline.
$element['content']['#prefix'] = '<div>';
$element['content']['#suffix'] = '</div>';
}
drupal_alter('media_token_to_markup', $element, $tag_info, $settings);
Alex Bronstein
committed
return drupal_render($element);
}
/**
* Builds a map of media tags in the element being rendered to their rendered HTML.
*
* The map is stored in JS, so we can transform them when the editor is being displayed.
*/
function media_pre_render_text_format($element) {
// filter_process_format() copies properties to the expanded 'value' child
// element.
if (!isset($element['format'])) {
return $element;
}
$field = &$element['value'];
$settings = array(
'field' => $field['#id'],
);
$tagmap = _media_generate_tagMap($field['#value']);
if (isset($tagmap)) {
drupal_add_js(array('tagmap' => $tagmap), 'setting');
}
return $element;
}
/**
* Generates an array of [inline tags] => <html> to be used in filter
* replacement and to add the mapping to JS.
* @param
* The String containing text and html markup of textarea
* @return
* An associative array with tag code as key and html markup as the value.
*
* @see media_process_form()
* @see media_token_to_markup()
*/
function _media_generate_tagMap($text) {
// Making $tagmap static as this function is called many times and
// adds duplicate markup for each tag code in Drupal.settings JS,
// so in media_process_form it adds something like tagCode:<markup>,
// <markup> and when we replace in attach see two duplicate images
// for one tagCode. Making static would make function remember value
// between function calls. Since media_process_form is multiple times
// with same form, this function is also called multiple times.
static $tagmap = array();
preg_match_all("/\[\[.*?\]\]/s", $text, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
// We see if tagContent is already in $tagMap, if not we add it
// to $tagmap. If we return an empty array, we break embeddings of the same
// media multiple times.
if (empty($tagmap[$match[0]])) {
// @TODO: Total HACK, but better than nothing.
// We should find a better way of cleaning this up.
if ($markup_for_media = media_token_to_markup($match, TRUE)) {
$tagmap[$match[0]] = $markup_for_media;
$missing = file_create_url(drupal_get_path('module', 'media') . '/images/icons/default/image-x-generic.png');
$tagmap[$match[0]] = '<div><img src="' . $missing . '" width="100px" height="100px"/></div>';
}
}
}
return $tagmap;
}
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/**
* Return a list of view modes allowed for a file embedded in the WYSIWYG.
*
* @param object $file
* A file entity.
*
* @return array
* An array of view modes that can be used on the file when embedded in the
* WYSIWYG.
*/
function media_get_wysiwyg_allowed_view_modes($file) {
$enabled_view_modes = &drupal_static(__FUNCTION__, array());
// @todo Add more caching for this.
if (!isset($enabled_view_modes[$file->type])) {
$enabled_view_modes[$file->type] = array();
// Add the default view mode by default.
$enabled_view_modes[$file->type]['default'] = array('label' => t('Default'), 'custom settings' => TRUE);
$entity_info = entity_get_info('file');
$view_mode_settings = field_view_mode_settings('file', $file->type);
foreach ($entity_info['view modes'] as $view_mode => $view_mode_info) {
// Do not show view modes that don't have their own settings and will
// only fall back to the default view mode.
if (empty($view_mode_settings[$view_mode]['custom_settings'])) {
continue;
}
// Don't present the user with an option to choose a view mode in which
// the file is hidden.
$extra_fields = field_extra_fields_get_display('file', $file->type, $view_mode);
if (empty($extra_fields['file']['visible'])) {
continue;
}
// Add the view mode to the list of enabled view modes.
$enabled_view_modes[$file->type][$view_mode] = $view_mode_info;
}
}
$view_modes = $enabled_view_modes[$file->type];
drupal_alter('media_wysiwyg_allowed_view_modes', $view_modes, $file);
return $view_modes;
}
* Allows the user to pick a format for their media file.
* Can also have additional params depending on the media type.
Alex Bronstein
committed
function media_format_form($form, $form_state, $file) {
$form = array();
Alex Bronstein
committed
$form['#media'] = $file;
$view_modes = media_get_wysiwyg_allowed_view_modes($file);
$formats = $options = array();
Alex Bronstein
committed
foreach ($view_modes as $view_mode => $view_mode_info) {
//@TODO: Display more verbose information about which formatter and what it does.
Alex Bronstein
committed
$options[$view_mode] = $view_mode_info['label'];
$element = media_get_file_without_label($file, $view_mode, array('wysiwyg' => TRUE));
Jacob Singh
committed
// Make a pretty name out of this.
Alex Bronstein
committed
$formats[$view_mode] = drupal_render($element);
// Add the previews back into the form array so they can be altered.
$form['#formats'] = &$formats;
if (!count($formats)) {
throw new Exception('Unable to continue, no available formats for displaying media.');
return;
}
$default_view_mode = media_variable_get('wysiwyg_default_view_mode');
if (!isset($formats[$default_view_mode])) {
$default_view_mode = key($formats);
}
// Add the previews by reference so that they can easily be altered by
// changing $form['#formats'].
$settings['media']['formatFormFormats'] = &$formats;
$form['#attached']['js'][] = array('data' => $settings, 'type' => 'setting');
// Add the required libraries, JavaScript and CSS for the form.
$form['#attached']['library'][] = array('media', 'media_base');
$form['#attached']['library'][] = array('system', 'form');
$form['#attached']['js'][] = drupal_get_path('module', 'media') . '/js/media.format_form.js';
$form['heading'] = array(
'#type' => 'markup',
'#prefix' => '<h1 class="title">',
'#suffix' => '</h1>',
Alex Bronstein
committed
'#markup' => t('Embedding %filename', array('%filename' => $file->filename)),
Alex Bronstein
committed
$preview = media_get_thumbnail_preview($file);
$form['preview'] = array(
'#type' => 'markup',
'#title' => check_plain(basename($file->uri)),
'#markup' => drupal_render($preview),
);
// These will get passed on to WYSIWYG
$form['options'] = array(
'#type' => 'fieldset',
'#title' => t('options'),
);
$form['options']['format'] = array(
'#type' => 'select',
'#title' => t('Current format is'),
'#options' => $options,
'#default_value' => $default_view_mode
if ($file->type === 'image') {
$form['options']['alt'] = array(
'#type' => 'textfield',
'#title' => t('Alternate text'),
'#description' => t('This text will be used by screen readers, search engines, or when the image cannot be loaded.'),
'#default_value' => isset($file->field_file_image_alt_text['und'][0]['safe_value']) ? $file->field_file_image_alt_text['und'][0]['safe_value'] : '',
);
$form['options']['title'] = array(
'#type' => 'textfield',
'#title' => t('Title'),
'#description' => t('The title is used as a tool tip when the user hovers the mouse over the image.'),
'#default_value' => isset($file->field_file_image_title_text['und'][0]['safe_value']) ? $file->field_file_image_title_text['und'][0]['safe_value'] : '',
);
}
// Similar to a form_alter, but we want this to run first so that media.types.inc
// can add the fields specific to a given type (like alt tags on media).
// If implemented as an alter, this might not happen, making other alters not
// be able to work on those fields.
// @TODO: We need to pass in existing values for those attributes.
Alex Bronstein
committed
drupal_alter('media_format_form_prepare', $form, $form_state, $file);
if (!element_children($form['options'])) {
$form['options']['#attributes'] = array('style' => 'display:none');
}
return $form;
}
/**
Alex Bronstein
committed
* Returns a drupal_render() array for just the file portion of a file entity.
Alex Bronstein
committed
* Optional custom settings can override how the file is displayed.
Alex Bronstein
committed
function media_get_file_without_label($file, $view_mode, $settings = array()) {
$file->override = $settings;
$element = file_view_file($file, $view_mode);
// The formatter invoked by file_view_file() can use $file->override to
// customize the returned render array to match the requested settings. To
// support simple formatters that don't do this, set the element attributes to
// what was requested, but not if the formatter applied its own logic for
// element attributes.
if (!isset($element['#attributes']) && isset($settings['attributes'])) {
$element['#attributes'] = $settings['attributes'];
Alex Bronstein
committed
// While this function may be called for any file type, images are a common
// use-case. theme_image() and theme_image_style() require the 'alt'
// attribute to be passed separately from the 'attributes' array (see
// http://drupal.org/node/999338). Until that's fixed, implement this
// special-case logic. Image formatters using other theme functions are
// responsible for their own 'alt' attribute handling. See
// theme_media_formatter_large_icon() for an example.
if (isset($settings['attributes']['alt']) && !isset($element['#alt']) && isset($element['#theme']) && in_array($element['#theme'], array('image', 'image_style'))) {
$element['#alt'] = $settings['attributes']['alt'];
}
Alex Bronstein
committed
}
return $element;
Alex Bronstein
committed
/**
* Clears caches that may be affected by the media filter.
*
Alex Bronstein
committed
* The media filter calls file_load(). This means that if a file object
Alex Bronstein
committed
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
* is updated, the check_markup() and field caches could return stale content.
* There are several possible approaches to deal with this:
* - Disable filter caching in media_filter_info(), this was found to cause a
* 30% performance hit from profiling four node teasers, due to both the
* media filter itself, and other filters that can't be cached.
* - Clear the filter and field caches whenever any media node is updated, this
* would ensure cache coherency but would reduce the effectiveness of those
* caches on high traffic sites with lots of media content updates.
* - The approach taken here: Record the fid of all media objects that are
* referenced by the media filter. Only clear the filter and field caches
* when one of these is updated, as opposed to all media objects.
* - @todo: consider an EntityFieldQuery to limit cache clearing to only those
* entities that use a text format with the media filter, possibly checking
* the contents of those fields to further limit this to fields referencing
* the media object being updated. This would need to be implemented
* carefully to avoid scalability issues with large result sets, and may
* not be worth the effort.
*
* @param $fid
* Optional media fid being updated. If not given, the cache will be cleared
* as long as any file is referenced.
*/
function media_filter_invalidate_caches($fid = FALSE) {
// If fid is passed, confirm that it has previously been referenced by the
// media filter. If not, clear the cache if the {media_filter_usage} has any
// valid records.
if (($fid && db_query('SELECT fid FROM {media_filter_usage} WHERE fid = :fid', array(':fid' => $fid))->fetchField()) || (!$fid && media_filter_usage_has_records())) {
// @todo: support entity cache, either via a hook, or using module_exists().
cache_clear_all('*', 'cache_filter', TRUE);
cache_clear_all('*', 'cache_field', TRUE);
}
}
/**
* Determines if the {media_filter_usage} table has any entries.
*/
function media_filter_usage_has_records() {
return (bool) db_query_range('SELECT 1 FROM {media_filter_usage} WHERE fid > :fid', 0, 1, array(':fid' => 0))->fetchField();
}
/**
* Tracks usage of media fids by the media filter.
*
* @param $fid
* The media fid.
*/
function media_filter_track_usage($fid) {
// This function only tracks when fids are found by the media filter.
// It would be impractical to check when formatted text is edited to remove
// references to fids, however by keeping a timestamp, we can implement
// rudimentary garbage collection in hook_flush_caches().
// However we only need to track that an fid has ever been referenced,
// not every time, so avoid updating this table more than once per month,
// per fid.
$timestamp = db_query('SELECT timestamp FROM {media_filter_usage} WHERE fid = :fid', array(':fid' => $fid))->fetchField();
if (!$timestamp || $timestamp <= REQUEST_TIME - 86400 * 30) {
db_merge('media_filter_usage')->key(array('fid' => $fid))->fields(array('fid' => $fid, 'timestamp' => REQUEST_TIME))->execute();
}
}