filter.module 54.9 KB
Newer Older
1 2 3
<?php
// $Id$

Dries's avatar
 
Dries committed
4 5 6 7 8
/**
 * @file
 * Framework for handling filtering of content.
 */

Dries's avatar
Dries committed
9
/**
10
 * Implements hook_help().
Dries's avatar
Dries committed
11
 */
12 13
function filter_help($path, $arg) {
  switch ($path) {
14
    case 'admin/help#filter':
15 16 17 18 19
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Filter module allows administrators to configure text formats. A text format defines the HTML tags, codes, and other input allowed in content and comments, and is a key feature in guarding against potentially damaging input from malicious users. For more information, see the online handbook entry for <a href="@filter">Filter module</a>.', array('@filter' => 'http://drupal.org/handbook/modules/filter/')) . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
20 21
      $output .= '<dt>' . t('Configuring text formats') . '</dt>';
      $output .= '<dd>' . t('Configure text formats on the <a href="@formats">Text formats page</a>. <strong>Improper text format configuration is a security risk</strong>. To ensure security, untrusted users should only have access to text formats that restrict them to either plain text or a safe set of HTML tags, since certain HTML tags can allow embedding malicious links or scripts in text. More trusted registered users may be granted permission to use less restrictive text formats in order to create rich content.', array('@formats' => url('admin/config/content/formats'))) . '</dd>';
22
      $output .= '<dt>' . t('Applying filters to text') . '</dt>';
23
      $output .= '<dd>' . t('Each text format uses filters to manipulate text, and most formats apply several different filters to text in a specific order. Each filter is designed for a specific purpose, and generally either adds, removes, or transforms elements within user-entered text before it is displayed. A filter does not change the actual content, but instead, modifies it temporarily before it is displayed. One filter may remove unapproved HTML tags, while another automatically adds HTML to make URLs display as clickable links.') . '</dd>';
24
      $output .= '<dt>' . t('Defining text formats') . '</dt>';
25
      $output .= '<dd>' . t('One format is included by default: <em>Plain text</em> (which removes all HTML tags). Additional formats may be created by your installation profile when you install Drupal, and more can be created by an administrator on the <a href="@text-formats">Text formats page</a>.', array('@text-formats' => url('admin/config/content/formats'))) . '</dd>';
26
      $output .= '<dt>' . t('Choosing a text format') . '</dt>';
27
      $output .= '<dd>' . t('Users with access to more than one text format can use the <em>Text format</em> fieldset to choose between available text formats when creating or editing multi-line content. Administrators can define the text formats available to each user role, and control the order of formats listed in the <em>Text format</em> fieldset on the <a href="@text-formats">Text formats page</a>.', array('@text-formats' => url('admin/config/content/formats'))) . '</dd>';
28
      $output .= '</dl>';
29
      return $output;
30

31
    case 'admin/config/content/formats':
32 33
      $output = '<p>' . t('Text formats define the HTML tags, code, and other formatting that can be used when entering text. <strong>Improper text format configuration is a security risk</strong>. Learn more on the <a href="@filterhelp">Filter module help page</a>.', array('@filterhelp' => url('admin/help/filter'))) . '</p>';
      $output .= '<p>' . t('Text formats are presented on content editing pages in the order defined on this page.') . '</p>';
34
      return $output;
35

36
    case 'admin/config/content/formats/%':
37
      $output = '<p>' . t('A text format contains filters that change the user input, for example stripping out malicious HTML or making URLs clickable. Filters are executed from top to bottom and the order is important, since one filter may prevent another filter from doing its job. For example, when URLs are converted into links before disallowed HTML tags are removed, all links may be removed. When this happens, the order of filters may need to be re-arranged.') . '</p>';
38
      return $output;
39 40 41
  }
}

42
/**
43
 * Implements hook_theme().
44 45 46 47
 */
function filter_theme() {
  return array(
    'filter_admin_overview' => array(
48
      'render element' => 'form',
49
      'file' => 'filter.admin.inc',
50
    ),
51 52
    'filter_admin_format_filter_order' => array(
      'render element' => 'element',
53
      'file' => 'filter.admin.inc',
54 55
    ),
    'filter_tips' => array(
56
      'variables' => array('tips' => NULL, 'long' => FALSE),
57
      'file' => 'filter.pages.inc',
58
    ),
59 60 61
    'text_format_wrapper' => array(
      'render element' => 'element',
    ),
62
    'filter_tips_more_info' => array(
63
      'variables' => array(),
64
    ),
65
    'filter_guidelines' => array(
66
      'variables' => array('format' => NULL),
67
    ),
68 69 70
  );
}

71 72 73 74 75 76 77 78 79 80 81 82 83 84
/**
 * Implements hook_element_info().
 *
 * @see filter_process_format()
 */
function filter_element_info() {
  $type['text_format'] = array(
    '#process' => array('filter_process_format'),
    '#base_type' => 'textarea',
    '#theme_wrappers' => array('text_format_wrapper'),
  );
  return $type;
}

85
/**
86
 * Implements hook_menu().
87
 */
88
function filter_menu() {
89 90 91 92 93 94 95
  $items['filter/tips'] = array(
    'title' => 'Compose tips',
    'page callback' => 'filter_tips_long',
    'access callback' => TRUE,
    'type' => MENU_SUGGESTED_ITEM,
    'file' => 'filter.pages.inc',
  );
96
  $items['admin/config/content/formats'] = array(
97
    'title' => 'Text formats',
98
    'description' => 'Configure how content input by users is filtered, including allowed HTML tags. Also allows enabling of module-provided filters.',
99 100 101
    'page callback' => 'drupal_get_form',
    'page arguments' => array('filter_admin_overview'),
    'access arguments' => array('administer filters'),
102
    'file' => 'filter.admin.inc',
103
  );
104
  $items['admin/config/content/formats/list'] = array(
105
    'title' => 'List',
106 107
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
108
  $items['admin/config/content/formats/add'] = array(
109
    'title' => 'Add text format',
110
    'page callback' => 'filter_admin_format_page',
111
    'access arguments' => array('administer filters'),
112
    'type' => MENU_LOCAL_ACTION,
113
    'weight' => 1,
114
    'file' => 'filter.admin.inc',
115
  );
116
  $items['admin/config/content/formats/%filter_format'] = array(
117
    'type' => MENU_CALLBACK,
118
    'title callback' => 'filter_admin_format_title',
119
    'title arguments' => array(4),
120
    'page callback' => 'filter_admin_format_page',
121
    'page arguments' => array(4),
122
    'access arguments' => array('administer filters'),
123
    'file' => 'filter.admin.inc',
124
  );
125
  $items['admin/config/content/formats/%filter_format/delete'] = array(
126 127
    'title' => 'Delete text format',
    'page callback' => 'drupal_get_form',
128
    'page arguments' => array('filter_admin_delete', 4),
129 130
    'access callback' => '_filter_delete_format_access',
    'access arguments' => array(4),
131 132 133
    'type' => MENU_CALLBACK,
    'file' => 'filter.admin.inc',
  );
134 135 136
  return $items;
}

137 138 139 140 141 142 143 144 145 146 147 148 149 150
/**
 * Access callback for deleting text formats.
 *
 * @param $format
 *   A text format object.
 * @return
 *   TRUE if the text format can be deleted by the current user, FALSE
 *   otherwise.
 */
function _filter_delete_format_access($format) {
  // The fallback format can never be deleted.
  return user_access('administer filters') && ($format->format != filter_fallback_format());
}

151 152 153
/**
 * Load a text format object from the database.
 *
154
 * @param $format_id
155 156 157 158 159
 *   The format ID.
 *
 * @return
 *   A fully-populated text format object.
 */
160
function filter_format_load($format_id) {
161
  $formats = filter_formats();
162
  return isset($formats[$format_id]) ? $formats[$format_id] : FALSE;
163 164
}

165 166 167 168
/**
 * Save a text format object to the database.
 *
 * @param $format
169 170 171 172
 *   A format object using the properties:
 *   - 'name': The title of the text format.
 *   - 'format': (optional) The internal ID of the text format. If omitted, a
 *     new text format is created.
173 174
 *   - 'weight': (optional) The weight of the text format, which controls its
 *     placement in text format lists. If omitted, the weight is set to 0.
175
 *   - 'filters': (optional) An associative, multi-dimensional array of filters
176 177 178 179 180 181 182 183
 *     assigned to the text format, keyed by the name of each filter and using
 *     the properties:
 *     - 'weight': (optional) The weight of the filter in the text format. If
 *       omitted, either the currently stored weight is retained (if there is
 *       one), or the filter is assigned a weight of 10, which will usually
 *       put it at the bottom of the list.
 *     - 'status': (optional) A boolean indicating whether the filter is
 *       enabled in the text format. If omitted, the filter will be disabled.
184 185
 *     - 'settings': (optional) An array of configured settings for the filter.
 *       See hook_filter_info() for details.
186
 */
187
function filter_format_save(&$format) {
188
  $format->name = trim($format->name);
189
  $format->cache = _filter_format_is_cacheable($format);
190 191 192

  // Add a new text format.
  if (empty($format->format)) {
193
    $return = drupal_write_record('filter_format', $format);
194 195
  }
  else {
196
    $return = drupal_write_record('filter_format', $format, 'format');
197 198
  }

199
  $filter_info = filter_get_filters();
200
  // Programmatic saves may not contain any filters.
201 202 203
  if (!isset($format->filters)) {
    $format->filters = array();
  }
204
  foreach ($filter_info as $name => $filter) {
205
    // Add new filters without weight to the bottom.
206
    if (!isset($format->filters[$name]['weight'])) {
207
      $format->filters[$name]['weight'] = 10;
208 209 210 211 212 213 214 215 216
    }
    $format->filters[$name]['status'] = isset($format->filters[$name]['status']) ? $format->filters[$name]['status'] : 0;
    $format->filters[$name]['module'] = $filter['module'];

    // If settings were passed, only ensure default settings.
    if (isset($format->filters[$name]['settings'])) {
      if (isset($filter['default settings'])) {
        $format->filters[$name]['settings'] = array_merge($filter['default settings'], $format->filters[$name]['settings']);
      }
217
    }
218 219 220 221 222 223 224 225 226 227 228
    // Otherwise, use default settings or fall back to an empty array.
    else {
      $format->filters[$name]['settings'] = isset($filter['default settings']) ? $filter['default settings'] : array();
    }

    $fields = array();
    $fields['weight'] = $format->filters[$name]['weight'];
    $fields['status'] = $format->filters[$name]['status'];
    $fields['module'] = $format->filters[$name]['module'];
    $fields['settings'] = serialize($format->filters[$name]['settings']);

229 230 231 232 233 234 235
    db_merge('filter')
      ->key(array(
        'format' => $format->format,
        'name' => $name,
      ))
      ->fields($fields)
      ->execute();
236 237
  }

238
  if ($return == SAVED_NEW) {
239 240 241 242
    module_invoke_all('filter_format_insert', $format);
  }
  else {
    module_invoke_all('filter_format_update', $format);
243 244 245 246 247 248 249 250
    // Explicitly indicate that the format was updated. We need to do this
    // since if the filters were updated but the format object itself was not,
    // the call to drupal_write_record() above would not return an indication
    // that anything had changed.
    $return = SAVED_UPDATED;

    // Clear the filter cache whenever a text format is updated.
    cache_clear_all($format->format . ':', 'cache_filter', TRUE);
251 252
  }

253
  filter_formats_reset();
254

255
  return $return;
256 257 258 259 260 261
}

/**
 * Delete a text format.
 *
 * @param $format
262
 *   The text format object to be deleted.
263 264 265
 */
function filter_format_delete($format) {
  db_delete('filter_format')
266
    ->condition('format', $format->format)
267 268
    ->execute();
  db_delete('filter')
269
    ->condition('format', $format->format)
270 271
    ->execute();

272
  // Allow modules to react on text format deletion.
273 274
  $fallback = filter_format_load(filter_fallback_format());
  module_invoke_all('filter_format_delete', $format, $fallback);
275

276
  filter_formats_reset();
277
  cache_clear_all($format->format . ':', 'cache_filter', TRUE);
278 279
}

280
/**
281
 * Display a text format form title.
282 283 284 285 286
 */
function filter_admin_format_title($format) {
  return $format->name;
}

287
/**
288
 * Implements hook_permission().
289
 */
290
function filter_permission() {
291
  $perms['administer filters'] = array(
292
    'title' => t('Administer text formats and filters'),
293
    'restrict access' => TRUE,
294
  );
295 296 297 298 299 300 301 302

  // Generate permissions for each text format. Warn the administrator that any
  // of them are potentially unsafe.
  foreach (filter_formats() as $format) {
    $permission = filter_permission_name($format);
    if (!empty($permission)) {
      // Only link to the text format configuration page if the user who is
      // viewing this will have access to that page.
303
      $format_name_replacement = user_access('administer filters') ? l($format->name, 'admin/config/content/formats/' . $format->format) : drupal_placeholder(array('text' => $format->name));
304
      $perms[$permission] = array(
305
        'title' => t("Use the !text_format text format", array('!text_format' => $format_name_replacement,)),
306
        'description' => drupal_placeholder(array('text' => t('Warning: This permission may have security implications depending on how the text format is configured.'))),
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
      );
    }
  }
  return $perms;
}

/**
 * Returns the machine-readable permission name for a provided text format.
 *
 * @param $format
 *   An object representing a text format.
 * @return
 *   The machine-readable permission name, or FALSE if the provided text format
 *   is malformed or is the fallback format (which is available to all users).
 */
function filter_permission_name($format) {
  if (isset($format->format) && $format->format != filter_fallback_format()) {
    return 'use text format ' . $format->format;
  }
  return FALSE;
327 328
}

329
/**
330
 * Implements hook_cron().
331 332 333 334 335 336 337
 *
 * Expire outdated filter cache entries
 */
function filter_cron() {
  cache_clear_all(NULL, 'cache_filter');
}

338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
/**
 * Implements hook_modules_enabled().
 */
function filter_modules_enabled($modules) {
  // Reset the static cache of module-provided filters, in case any of the
  // newly enabled modules defines a new filter or alters existing ones.
  drupal_static_reset('filter_get_filters');
}

/**
 * Implements hook_modules_disabled().
 */
function filter_modules_disabled($modules) {
  // Reset the static cache of module-provided filters, in case any of the
  // newly disabled modules defined or altered any filters.
  drupal_static_reset('filter_get_filters');
}

Dries's avatar
Dries committed
356
/**
357
 * Retrieve a list of text formats, ordered by weight.
358 359
 *
 * @param $account
360 361
 *   (optional) If provided, only those formats that are allowed for this user
 *   account will be returned. All formats will be returned otherwise.
362
 * @return
363 364 365 366
 *   An array of text format objects, keyed by the format ID and ordered by
 *   weight.
 *
 * @see filter_formats_reset()
Dries's avatar
Dries committed
367
 */
368
function filter_formats($account = NULL) {
369
  $formats = &drupal_static(__FUNCTION__, array());
370

371 372
  // Statically cache all existing formats upfront.
  if (!isset($formats['all'])) {
373 374 375 376 377 378
    $formats['all'] = db_select('filter_format', 'ff')
      ->addTag('translatable')
      ->fields('ff')
      ->orderBy('weight')
      ->execute()
      ->fetchAllAssoc('format');
379
  }
380

381 382 383 384 385 386
  // Build a list of user-specific formats.
  if (isset($account) && !isset($formats['user'][$account->uid])) {
    $formats['user'][$account->uid] = array();
    foreach ($formats['all'] as $format) {
      if (filter_access($format, $account)) {
        $formats['user'][$account->uid][$format->format] = $format;
387 388
      }
    }
389 390 391 392
  }

  return isset($account) ? $formats['user'][$account->uid] : $formats['all'];
}
393

394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415
/**
 * Resets the static cache of all text formats.
 *
 * @see filter_formats()
 */
function filter_formats_reset() {
  drupal_static_reset('filter_list_format');
  drupal_static_reset('filter_formats');
}

/**
 * Retrieves a list of roles that are allowed to use a given text format.
 *
 * @param $format
 *   An object representing the text format.
 * @return
 *   An array of role names, keyed by role ID.
 */
function filter_get_roles_by_format($format) {
  // Handle the fallback format upfront (all roles have access to this format).
  if ($format->format == filter_fallback_format()) {
    return user_roles();
416
  }
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
  // Do not list any roles if the permission does not exist.
  $permission = filter_permission_name($format);
  return !empty($permission) ? user_roles(FALSE, $permission) : array();
}

/**
 * Retrieves a list of text formats that are allowed for a given role.
 *
 * @param $rid
 *   The user role ID to retrieve text formats for.
 * @return
 *   An array of text format objects that are allowed for the role, keyed by
 *   the text format ID and ordered by weight.
 */
function filter_get_formats_by_role($rid) {
  $formats = array();
  foreach (filter_formats() as $format) {
    $roles = filter_get_roles_by_format($format);
    if (isset($roles[$rid])) {
      $formats[$format->format] = $format;
    }
438
  }
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
  return $formats;
}

/**
 * Returns the ID of the default text format for a particular user.
 *
 * The default text format is the first available format that the user is
 * allowed to access, when the formats are ordered by weight. It should
 * generally be used as a default choice when presenting the user with a list
 * of possible text formats (for example, in a node creation form).
 *
 * Conversely, when existing content that does not have an assigned text format
 * needs to be filtered for display, the default text format is the wrong
 * choice, because it is not guaranteed to be consistent from user to user, and
 * some trusted users may have an unsafe text format set by default, which
 * should not be used on text of unknown origin. Instead, the fallback format
 * returned by filter_fallback_format() should be used, since that is intended
 * to be a safe, consistent format that is always available to all users.
 *
 * @param $account
 *   (optional) The user account to check. Defaults to the currently logged-in
 *   user.
 * @return
 *   The ID of the user's default text format.
 *
 * @see filter_fallback_format()
 */
function filter_default_format($account = NULL) {
  global $user;
  if (!isset($account)) {
    $account = $user;
  }
  // Get a list of formats for this user, ordered by weight. The first one
  // available is the user's default format.
473 474
  $formats = filter_formats($account);
  $format = reset($formats);
475 476 477 478 479
  return $format->format;
}

/**
 * Returns the ID of the fallback text format that all users have access to.
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
 *
 * The fallback text format is a regular text format in every respect, except
 * it does not participate in the filter permission system and cannot be
 * deleted. It needs to exist because any user who has permission to create
 * formatted content must always have at least one text format they can use.
 *
 * Because the fallback format is available to all users, it should always be
 * configured securely. For example, when the Filter module is installed, this
 * format is initialized to output plain text. Installation profiles and site
 * administrators have the freedom to configure it further.
 *
 * When a text format is deleted, all content that previously had that format
 * assigned should be switched to the fallback format. To facilitate this,
 * Drupal passes in the fallback format object as one of the parameters of
 * hook_filter_format_delete().
 *
 * Note that the fallback format is completely distinct from the default
 * format, which differs per user and is simply the first format which that
 * user has access to. The default and fallback formats are only guaranteed to
 * be the same for users who do not have access to any other format; otherwise,
 * the fallback format's weight determines its placement with respect to the
 * user's other formats.
 *
 * @see hook_filter_format_delete()
 * @see filter_default_format()
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
 */
function filter_fallback_format() {
  // This variable is automatically set in the database for all installations
  // of Drupal. In the event that it gets deleted somehow, there is no safe
  // default to return, since we do not want to risk making an existing (and
  // potentially unsafe) text format on the site automatically available to all
  // users. Returning NULL at least guarantees that this cannot happen.
  return variable_get('filter_fallback_format');
}

/**
 * Returns the title of the fallback text format.
 */
function filter_fallback_format_title() {
  $fallback_format = filter_format_load(filter_fallback_format());
  return filter_admin_format_title($fallback_format);
521
}
522

523
/**
524
 * Return a list of all filters provided by modules.
525
 */
526 527 528 529 530 531 532
function filter_get_filters() {
  $filters = &drupal_static(__FUNCTION__, array());

  if (empty($filters)) {
    foreach (module_implements('filter_info') as $module) {
      $info = module_invoke($module, 'filter_info');
      if (isset($info) && is_array($info)) {
533 534
        // Assign the name of the module implementing the filters and ensure
        // default values.
535 536
        foreach (array_keys($info) as $name) {
          $info[$name]['module'] = $module;
537 538 539 540
          $info[$name] += array(
            'description' => '',
            'weight' => 0,
          );
541
        }
542
        $filters = array_merge($filters, $info);
543
      }
Dries's avatar
 
Dries committed
544
    }
545 546
    // Allow modules to alter filter definitions.
    drupal_alter('filter_info', $filters);
Dries's avatar
 
Dries committed
547

548 549
    uasort($filters, '_filter_list_cmp');
  }
550 551 552 553 554 555 556 557

  return $filters;
}

/**
 * Helper function for sorting the filter list by filter name.
 */
function _filter_list_cmp($a, $b) {
558
  return strcmp($a['title'], $b['title']);
Dries's avatar
 
Dries committed
559 560
}

Dries's avatar
Dries committed
561
/**
562
 * Check if text in a certain text format is allowed to be cached.
563 564 565 566 567 568 569 570 571
 *
 * This function can be used to check whether the result of the filtering
 * process can be cached. A text format may allow caching depending on the
 * filters enabled.
 *
 * @param $format_id
 *   The text format ID to check.
 * @return
 *   TRUE if the given text format allows caching, FALSE otherwise.
Dries's avatar
Dries committed
572
 */
573
function filter_format_allowcache($format_id) {
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598
  $format = filter_format_load($format_id);
  return !empty($format->cache);
}

/**
 * Helper function to determine whether the output of a given text format can be cached.
 *
 * The output of a given text format can be cached when all enabled filters in
 * the text format allow caching.
 *
 * @param $format
 *   The text format object to check.
 * @return
 *   TRUE if all the filters enabled in the given text format allow caching,
 *   FALSE otherwise.
 *
 * @see filter_format_save()
 */
function _filter_format_is_cacheable($format) {
  if (empty($format->filters)) {
    return TRUE;
  }
  $filter_info = filter_get_filters();
  foreach ($format->filters as $name => $filter) {
    // By default, 'cache' is TRUE for all filters unless specified otherwise.
599
    if (!empty($filter['status']) && isset($filter_info[$name]['cache']) && !$filter_info[$name]['cache']) {
600 601
      return FALSE;
    }
602
  }
603
  return TRUE;
604 605 606
}

/**
607
 * Retrieve a list of filters for a given text format.
608
 *
609 610 611 612 613
 * Note that this function returns all associated filters regardless of whether
 * they are enabled or disabled. All functions working with the filter
 * information outside of filter administration should test for $filter->status
 * before performing actions with the filter.
 *
614
 * @param $format_id
615
 *   The format ID to retrieve filters for.
616
 *
617
 * @return
618 619
 *   An array of filter objects associated to the given text format, keyed by
 *   filter name.
620
 */
621
function filter_list_format($format_id) {
622
  $filters = &drupal_static(__FUNCTION__, array());
623
  $filter_info = filter_get_filters();
624

625 626 627 628 629 630 631
  if (!isset($filters['all'])) {
    $result = db_query('SELECT * FROM {filter} ORDER BY weight, module, name');
    foreach ($result as $record) {
      $filters['all'][$record->format][$record->name] = $record;
    }
  }

632
  if (!isset($filters[$format_id])) {
633
    $format_filters = array();
634
    foreach ($filters['all'][$format_id] as $name => $filter) {
635 636
      if (isset($filter_info[$name])) {
        $filter->title = $filter_info[$name]['title'];
637
        // Unpack stored filter settings.
638
        $filter->settings = (isset($filter->settings) ? unserialize($filter->settings) : array());
639

640
        $format_filters[$name] = $filter;
Dries's avatar
 
Dries committed
641 642
      }
    }
643
    $filters[$format_id] = $format_filters;
Dries's avatar
 
Dries committed
644 645
  }

646
  return isset($filters[$format_id]) ? $filters[$format_id] : array();
647 648
}

649
/**
650
 * Run all the enabled filters on a piece of text.
651
 *
652
 * Note: Because filters can inject JavaScript or execute PHP code, security is
653 654 655 656
 * vital here. When a user supplies a text format, you should validate it using
 * filter_access() before accepting/using it. This is normally done in the
 * validation stage of the Form API. You should for example never make a preview
 * of content in a disallowed format.
657 658
 *
 * @param $text
659
 *   The text to be filtered.
660 661
 * @param $format_id
 *   The format id of the text to be filtered. If no format is assigned, the
662
 *   fallback format will be used.
663
 * @param $langcode
664 665 666
 *   Optional: the language code of the text to be filtered, e.g. 'en' for
 *   English. This allows filters to be language aware so language specific
 *   text replacement can be implemented.
667 668 669 670
 * @param $cache
 *   Boolean whether to cache the filtered output in the {cache_filter} table.
 *   The caller may set this to FALSE when the output is already cached
 *   elsewhere to avoid duplicate cache lookups and storage.
Dries's avatar
Dries committed
671
 */
672 673 674
function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) {
  if (empty($format_id)) {
    $format_id = filter_fallback_format();
675
  }
676
  $format = filter_format_load($format_id);
677

678
  // Check for a cached version of this piece of text.
679
  $cache = $cache && !empty($format->cache);
680
  $cache_id = '';
681
  if ($cache) {
682
    $cache_id = $format->format . ':' . $langcode . ':' . md5($text);
683 684 685
    if ($cached = cache_get($cache_id, 'cache_filter')) {
      return $cached->data;
    }
686
  }
687

688 689 690
  // Convert all Windows and Mac newlines to a single newline, so filters only
  // need to deal with one possibility.
  $text = str_replace(array("\r\n", "\r"), "\n", $text);
Dries's avatar
 
Dries committed
691

692
  // Get a complete list of filters, ordered properly.
693
  $filters = filter_list_format($format->format);
694
  $filter_info = filter_get_filters();
695

696
  // Give filters the chance to escape HTML-like data such as code or formulas.
697
  foreach ($filters as $name => $filter) {
698 699 700
    if ($filter->status && isset($filter_info[$name]['prepare callback']) && function_exists($filter_info[$name]['prepare callback'])) {
      $function = $filter_info[$name]['prepare callback'];
      $text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
701
    }
702
  }
703

704
  // Perform filtering.
705
  foreach ($filters as $name => $filter) {
706 707 708
    if ($filter->status && isset($filter_info[$name]['process callback']) && function_exists($filter_info[$name]['process callback'])) {
      $function = $filter_info[$name]['process callback'];
      $text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
Dries's avatar
Dries committed
709 710
    }
  }
711 712

  // Store in cache with a minimum expiration time of 1 day.
713
  if ($cache) {
714
    cache_set($cache_id, $text, 'cache_filter', REQUEST_TIME + (60 * 60 * 24));
Dries's avatar
Dries committed
715 716 717 718 719 720
  }

  return $text;
}

/**
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748
 * Expands an element into a base element with text format selector attached.
 *
 * The form element will be expanded into two separate form elements, one
 * holding the original element, and the other holding the text format selector:
 * - value: Holds the original element, having its #type changed to the value of
 *   #base_type or 'textarea' by default.
 * - format: Holds the text format fieldset and the text format selection, using
 *   the text format id specified in #format or the user's default format by
 *   default, if NULL.
 *
 * Since most modules expect the value of the new 'format' element *next* to the
 * original element, filter_process_format() utilizes an #after_build to move
 * the values of the children of the 'text_format' element so as to let the
 * submitted form values appear as if they were located on the same level.
 * For example, considering the input values:
 * @code
 *   $form_state['input']['body']['value'] = 'foo';
 *   $form_state['input']['body']['format'] = 'foo';
 * @endcode
 * The #after_build will process them into:
 * @code
 *   $form_state['values']['body'] = 'foo';
 *   $form_state['values']['format'] = 'foo';
 * @endcode
 *
 * If multiple text format-enabled elements are required on the same level of
 * the form structure, modules can set custom #parents on the original element.
 * Alternatively, the #after_build may be unset through a subsequent #process
749 750 751
 * callback. If the default #after_build is not invoked and no custom processing
 * occurs, then the submitted form values will appear like in the
 * $form_state['input'] array above.
752 753 754 755 756 757 758 759 760
 *
 * @see filter_form_after_build()
 *
 * @param $element
 *   The form element to process. Properties used:
 *   - #base_type: The form element #type to use for the 'value' element.
 *     'textarea' by default.
 *   - #format: (optional) The text format id to preselect. If 0, NULL, or not
 *     set, the default format for the current user will be used.
761
 *
Dries's avatar
Dries committed
762
 * @return
763
 *   The expanded element.
Dries's avatar
Dries committed
764
 */
765
function filter_process_format($element) {
766 767
  global $user;

768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788
  // Ensure that children appear as subkeys of this element.
  $element['#tree'] = TRUE;
  $blacklist = array(
    // Make form_builder() regenerate child properties.
    '#parents',
    '#id',
    '#name',
    // Do not copy this #process function to prevent form_builder() from
    // recursing infinitely.
    '#process',
    // Description is handled by theme_text_format_wrapper().
    '#description',
    // Ensure proper ordering of children.
    '#weight',
    // Properties already processed for the parent element.
    '#prefix',
    '#suffix',
    '#attached',
    '#processed',
    '#theme_wrappers',
  );
789
  // Move this element into sub-element 'value'.
790
  unset($element['value']);
791 792 793
  foreach (element_properties($element) as $key) {
    if (!in_array($key, $blacklist)) {
      $element['value'][$key] = $element[$key];
794
    }
795 796
  }

797 798 799 800 801 802 803
  $element['value']['#type'] = $element['#base_type'];
  $element['value'] += element_info($element['#base_type']);

  // Turn original element into a text format wrapper.
  $path = drupal_get_path('module', 'filter');
  $element['#attached']['js'][] = $path . '/filter.js';
  $element['#attached']['css'][] = $path . '/filter.css';
804

805 806
  // Apply default #after_build behavior.
  $element['#after_build'][] = 'filter_form_after_build';
807

808 809
  // Setup child container for the text format widget.
  $element['format'] = array(
810
    '#type' => 'fieldset',
811
    '#attributes' => array('class' => array('filter-wrapper')),
812
  );
813 814 815 816 817 818

  // Prepare text format guidelines.
  $element['format']['guidelines'] = array(
    '#type' => 'container',
    '#attributes' => array('class' => array('filter-guidelines')),
    '#weight' => 20,
819
  );
820 821
  // Get a list of formats that the current user has access to.
  $formats = filter_formats($user);
822 823
  foreach ($formats as $format) {
    $options[$format->format] = $format->name;
824 825 826
    $element['format']['guidelines'][$format->format] = array(
      '#theme' => 'filter_guidelines',
      '#format' => $format,
827
    );
Dries's avatar
Dries committed
828
  }
829 830 831 832 833 834

  // Use the default format for this user if none was selected.
  if (empty($element['#format'])) {
    $element['#format'] = filter_default_format($user);
  }
  $element['format']['format'] = array(
835 836 837
    '#type' => 'select',
    '#title' => t('Text format'),
    '#options' => $options,
838
    '#default_value' => $element['#format'],
839
    '#access' => count($formats) > 1,
840
    '#weight' => 10,
841
    '#attributes' => array('class' => array('filter-list')),
842
    '#parents' => array_merge($element['#parents'], array('format')),
843
  );
844 845 846 847 848 849

  $element['format']['help'] = array(
    '#type' => 'container',
    '#theme' => 'filter_tips_more_info',
    '#attributes' => array('class' => array('filter-help')),
    '#weight' => 0,
850 851
  );

852 853 854 855
  return $element;
}

/**
856
 * After build callback to move #type 'text_format' values up in $form_state.
857 858 859 860 861 862 863
 */
function filter_form_after_build($element, &$form_state) {
  // For text fields, the additional subkeys map 1:1 to field schema columns.
  if (isset($element['#columns'])) {
    return $element;
  }

864 865 866
  $parents = $element['#parents'];
  array_pop($parents);

867 868 869 870 871 872
  foreach (element_children($element) as $key) {
    $current_parents = $parents;
    switch ($key) {
      case 'value':
        form_set_value(array('#parents' => $element['#parents']), $element[$key]['#value'], $form_state);
        break;
873

874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909
      case 'format':
        $current_parents[] = $key;
        form_set_value(array('#parents' => $current_parents), $element['format']['format']['#value'], $form_state);
        break;

      default:
        $current_parents[] = $key;
        form_set_value(array('#parents' => $current_parents), $element[$key]['#value'], $form_state);
    }
  }
  return $element;
}

/**
 * Render a text format-enabled form element.
 *
 * @param $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #children, #description
 *
 * @return
 *   A string representing the form element.
 *
 * @ingroup themeable
 */
function theme_text_format_wrapper($variables) {
  $element = $variables['element'];
  $output = '<div class="text-format-wrapper">';
  $output .= $element['#children'];
  if (!empty($element['#description'])) {
    $output .= '<div class="description">' . $element['#description'] . '</div>';
  }
  $output .= "</div>\n";

  return $output;
Dries's avatar
Dries committed
910 911 912
}

/**
913
 * Checks if a user has access to a particular text format.
914 915
 *
 * @param $format
916
 *   An object representing the text format.
917 918 919 920 921 922
 * @param $account
 *   (optional) The user account to check access for; if omitted, the currently
 *   logged-in user is used.
 *
 * @return
 *   Boolean TRUE if the user is allowed to access the given format.
Dries's avatar
Dries committed
923
 */
924
function filter_access($format, $account = NULL) {
925 926 927
  global $user;
  if (!isset($account)) {
    $account = $user;
Dries's avatar
Dries committed
928
  }
929
  // Handle special cases up front. All users have access to the fallback
930 931
  // format.
  if ($format->format == filter_fallback_format()) {
932
    return TRUE;
Dries's avatar
Dries committed
933
  }
934 935 936 937
  // Check the permission if one exists; otherwise, we have a non-existent
  // format so we return FALSE.
  $permission = filter_permission_name($format);
  return !empty($permission) && user_access($permission, $account);
Dries's avatar
Dries committed
938
}
939

Dries's avatar
Dries committed
940 941 942
/**
 * Helper function for fetching filter tips.
 */
943
function _filter_tips($format_id, $long = FALSE) {
944 945 946
  global $user;

  $formats = filter_formats($user);
947
  $filter_info = filter_get_filters();
Dries's avatar
Dries committed
948 949 950

  $tips = array();

951
  // If only listing one format, extract it from the $formats array.
952 953
  if ($format_id != -1) {
    $formats = array($formats[$format_id]);
954 955
  }

Dries's avatar
Dries committed
956 957 958
  foreach ($formats as $format) {
    $filters = filter_list_format($format->format);
    $tips[$format->name] = array();
959
    foreach ($filters as $name => $filter) {