filter.module 46.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.
 */

9 10 11 12 13 14
/**
 * Special format ID which means "use the default format".
 *
 * This value can be passed to the filter APIs as a format ID: this is
 * equivalent to not passing an explicit format at all.
 */
15 16
define('FILTER_FORMAT_DEFAULT', 0);

Dries's avatar
Dries committed
17 18 19
/**
 * Implementation of hook_help().
 */
20 21
function filter_help($path, $arg) {
  switch ($path) {
22
    case 'admin/help#filter':
23 24 25 26
      $output = '<p>' . t("The filter module allows administrators to configure text input formats for use on your site. An input format defines the HTML tags, codes, and other input allowed in both content and comments, and is a key feature in guarding against potentially damaging input from malicious users. Two input formats included by default are <em>Filtered HTML</em> (which allows only an administrator-approved subset of HTML tags) and <em>Full HTML</em> (which allows the full set of HTML tags). Additional input formats may be created by an administrator.") . '</p>';
      $output .= '<p>' . t('Each input format uses filters to manipulate text, and most input 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 of a post, but instead, modifies it temporarily before it is displayed. A filter may remove unapproved HTML tags, for instance, while another automatically adds HTML to make links referenced in text clickable.') . '</p>';
      $output .= '<p>' . t('Users with access to more than one input format can use the <em>Input format</em> fieldset to choose between available input formats when creating or editing multi-line content. Administrators determine the input formats available to each user role, select a default input format, and control the order of formats listed in the <em>Input format</em> fieldset.') . '</p>';
      $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@filter">Filter module</a>.', array('@filter' => 'http://drupal.org/handbook/modules/filter/')) . '</p>';
27
      return $output;
28
    case 'admin/settings/filters':
29 30
      $output = '<p>' . t('Use the list below to review the input formats available to each user role, to select a default input format, and to control the order of formats listed in the <em>Input format</em> fieldset. (The <em>Input format</em> fieldset is displayed below textareas when users with access to more than one input format create multi-line content.) The input format selected as <em>Default</em> is available to all users and, unless another format is selected, is applied to all content. All input formats are available to users in roles with the "administer filters" permission.') . '</p>';
      $output .= '<p>' . t('Since input formats, if available, are presented in the same order as the list below, it may be helpful to arrange the formats in descending order of your preference for their use. To change the order of an input format, grab a drag-and-drop handle under the <em>Name</em> column and drag to a new location in the list. (Grab a handle by clicking and holding the mouse while hovering over a handle icon.) Remember that your changes will not be saved until you click the <em>Save changes</em> button at the bottom of the page.') . '</p>';
31
      return $output;
32
    case 'admin/settings/filters/%':
33
      return '<p>' . t('Every <em>filter</em> performs one particular change on the user input, for example stripping out malicious HTML or making URLs clickable. Choose which filters you want to apply to text in this input format. If you notice some filters are causing conflicts in the output, you can <a href="@rearrange">rearrange them</a>.', array('@rearrange' => url('admin/settings/filters/' . $arg[3] . '/order'))) . '</p>';
34
    case 'admin/settings/filters/%/configure':
35
      return '<p>' . t('If you cannot find the settings for a certain filter, make sure you have enabled it on the <a href="@url">edit tab</a> first.', array('@url' => url('admin/settings/filters/' . $arg[3]))) . '</p>';
36
    case 'admin/settings/filters/%/order':
37 38
      $output = '<p>' . t('Because of the flexible filtering system, you might encounter a situation where one filter prevents another from doing its job. For example: a word in an URL gets converted into a glossary term, before the URL can be converted to a clickable link. When this happens, rearrange the order of the filters.') . '</p>';
      $output .= '<p>' . t("Filters are executed from top-to-bottom. To change the order of the filters, modify the values in the <em>Weight</em> column or grab a drag-and-drop handle under the <em>Name</em> column and drag filters to new locations in the list. (Grab a handle by clicking and holding the mouse while hovering over a handle icon.) Remember that your changes will not be saved until you click the <em>Save configuration</em> button at the bottom of the page.") . '</p>';
39
      return $output;
40 41 42
  }
}

43
/**
44
 * Implementation of hook_theme().
45 46 47 48 49
 */
function filter_theme() {
  return array(
    'filter_admin_overview' => array(
      'arguments' => array('form' => NULL),
50
      'file' => 'filter.admin.inc',
51 52 53
    ),
    'filter_admin_order' => array(
      'arguments' => array('form' => NULL),
54
      'file' => 'filter.admin.inc',
55 56 57
    ),
    'filter_tips' => array(
      'arguments' => array('tips' => NULL, 'long' => FALSE, 'extra' => ''),
58
      'file' => 'filter.pages.inc',
59 60 61 62 63 64 65
    ),
    'filter_tips_more_info' => array(
      'arguments' => array(),
    ),
  );
}

66 67 68
/**
 * Implementation of hook_menu().
 */
69 70
function filter_menu() {
  $items['admin/settings/filters'] = array(
71 72
    'title' => 'Input formats',
    'description' => 'Configure how content input by users is filtered, including allowed HTML tags. Also allows enabling of module-provided filters.',
73 74 75 76 77
    'page callback' => 'drupal_get_form',
    'page arguments' => array('filter_admin_overview'),
    'access arguments' => array('administer filters'),
  );
  $items['admin/settings/filters/list'] = array(
78
    'title' => 'List',
79 80 81
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/settings/filters/add'] = array(
82
    'title' => 'Add input format',
83
    'page callback' => 'filter_admin_format_page',
84
    'access arguments' => array('administer filters'),
85 86 87 88
    'type' => MENU_LOCAL_TASK,
    'weight' => 1,
  );
  $items['admin/settings/filters/delete'] = array(
89
    'title' => 'Delete input format',
90 91
    'page callback' => 'drupal_get_form',
    'page arguments' => array('filter_admin_delete'),
92
    'access arguments' => array('administer filters'),
93 94 95
    'type' => MENU_CALLBACK,
  );
  $items['filter/tips'] = array(
96
    'title' => 'Compose tips',
97 98 99 100
    'page callback' => 'filter_tips_long',
    'access callback' => TRUE,
    'type' => MENU_SUGGESTED_ITEM,
  );
101
  $items['admin/settings/filters/%filter_format'] = array(
102
    'type' => MENU_CALLBACK,
103 104
    'title callback' => 'filter_admin_format_title',
    'title arguments' => array(3),
105 106
    'page callback' => 'filter_admin_format_page',
    'page arguments' => array(3),
107 108
    'access arguments' => array('administer filters'),
  );
109 110
  $items['admin/settings/filters/%filter_format/edit'] = array(
    'title' => 'Edit',
111 112 113
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => 0,
  );
114
  $items['admin/settings/filters/%filter_format/configure'] = array(
115
    'title' => 'Configure',
116 117
    'page callback' => 'filter_admin_configure_page',
    'page arguments' => array(3),
118
    'access arguments' => array('administer filters'),
119 120 121
    'type' => MENU_LOCAL_TASK,
    'weight' => 1,
  );
122
  $items['admin/settings/filters/%filter_format/order'] = array(
123
    'title' => 'Rearrange',
124 125
    'page callback' => 'filter_admin_order_page',
    'page arguments' => array(3),
126
    'access arguments' => array('administer filters'),
127 128 129
    'type' => MENU_LOCAL_TASK,
    'weight' => 2,
  );
130 131 132
  return $items;
}

133 134 135 136
function filter_format_load($arg) {
  return filter_formats($arg);
}

137 138 139 140 141 142 143
/**
 * Display a filter format form title.
 */
function filter_admin_format_title($format) {
  return $format->name;
}

144 145 146 147
/**
 * Implementation of hook_perm().
 */
function filter_perm() {
148
  return array(
149 150 151 152
    'administer filters' => array(
      'title' => t('Administer filters'),
      'description' => t('Manage input formats and filters, and select which roles may use them. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
    ),
153
  );
154 155
}

156 157 158 159 160 161 162 163 164
/**
 * Implementation of hook_cron().
 *
 * Expire outdated filter cache entries
 */
function filter_cron() {
  cache_clear_all(NULL, 'cache_filter');
}

165
/**
166
 * Implementation of hook_filter_tips().
167
 */
168
function filter_filter_tips($delta, $format, $long = FALSE) {
169
  global $base_url;
170 171
  switch ($delta) {
    case 0:
172
      if ($allowed_html = variable_get("allowed_html_$format", '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>')) {
173 174 175 176
        switch ($long) {
          case 0:
            return t('Allowed HTML tags: @tags', array('@tags' => $allowed_html));
          case 1:
177
            $output = '<p>' . t('Allowed HTML tags: @tags', array('@tags' => $allowed_html)) . '</p>';
178 179 180 181
            if (!variable_get("filter_html_help_$format", 1)) {
              return $output;
            }

182 183
            $output .= '<p>' . t('This site allows HTML content. While learning all of HTML may feel intimidating, learning how to use a very small number of the most basic HTML "tags" is very easy. This table provides examples for each tag that is enabled on this site.') . '</p>';
            $output .= '<p>' . t('For more information see W3C\'s <a href="@html-specifications">HTML Specifications</a> or use your favorite search engine to find other sites that explain HTML.', array('@html-specifications' => 'http://www.w3.org/TR/html/')) . '</p>';
184
            $tips = array(
185
              'a' => array( t('Anchors are used to make links to other pages.'), '<a href="' . $base_url . '">' . variable_get('site_name', 'Drupal') . '</a>'),
186
              'br' => array( t('By default line break tags are automatically added, so use this tag to add additional ones. Use of this tag is different because it is not used with an open/close pair like all the others. Use the extra " /" inside the tag to maintain XHTML 1.0 compatibility'), t('Text with <br />line break')),
187 188 189 190 191 192 193 194
              'p' => array( t('By default paragraph tags are automatically added, so use this tag to add additional ones.'), '<p>' . t('Paragraph one.') . '</p> <p>' . t('Paragraph two.') . '</p>'),
              'strong' => array( t('Strong'), '<strong>' . t('Strong') . '</strong>'),
              'em' => array( t('Emphasized'), '<em>' . t('Emphasized') . '</em>'),
              'cite' => array( t('Cited'), '<cite>' . t('Cited') . '</cite>'),
              'code' => array( t('Coded text used to show programming source code'), '<code>' . t('Coded') . '</code>'),
              'b' => array( t('Bolded'), '<b>' . t('Bolded') . '</b>'),
              'u' => array( t('Underlined'), '<u>' . t('Underlined') . '</u>'),
              'i' => array( t('Italicized'), '<i>' . t('Italicized') . '</i>'),
195 196
              'sup' => array( t('Superscripted'), t('<sup>Super</sup>scripted')),
              'sub' => array( t('Subscripted'), t('<sub>Sub</sub>scripted')),
197
              'pre' => array( t('Preformatted'), '<pre>' . t('Preformatted') . '</pre>'),
198 199
              'abbr' => array( t('Abbreviation'), t('<abbr title="Abbreviation">Abbrev.</abbr>')),
              'acronym' => array( t('Acronym'), t('<acronym title="Three-Letter Acronym">TLA</acronym>')),
200 201
              'blockquote' => array( t('Block quoted'), '<blockquote>' . t('Block quoted') . '</blockquote>'),
              'q' => array( t('Quoted inline'), '<q>' . t('Quoted inline') . '</q>'),
202
              // Assumes and describes tr, td, th.
203
              'table' => array( t('Table'), '<table> <tr><th>' . t('Table header') . '</th></tr> <tr><td>' . t('Table cell') . '</td></tr> </table>'),
204
              'tr' => NULL, 'td' => NULL, 'th' => NULL,
205 206
              'del' => array( t('Deleted'), '<del>' . t('Deleted') . '</del>'),
              'ins' => array( t('Inserted'), '<ins>' . t('Inserted') . '</ins>'),
207
               // Assumes and describes li.
208 209
              'ol' => array( t('Ordered list - use the &lt;li&gt; to begin each list item'), '<ol> <li>' . t('First item') . '</li> <li>' . t('Second item') . '</li> </ol>'),
              'ul' => array( t('Unordered list - use the &lt;li&gt; to begin each list item'), '<ul> <li>' . t('First item') . '</li> <li>' . t('Second item') . '</li> </ul>'),
210 211
              'li' => NULL,
              // Assumes and describes dt and dd.
212
              'dl' => array( t('Definition lists are similar to other HTML lists. &lt;dl&gt; begins the definition list, &lt;dt&gt; begins the definition term and &lt;dd&gt; begins the definition description.'), '<dl> <dt>' . t('First term') . '</dt> <dd>' . t('First definition') . '</dd> <dt>' . t('Second term') . '</dt> <dd>' . t('Second definition') . '</dd> </dl>'),
213
              'dt' => NULL, 'dd' => NULL,
214 215 216 217 218 219
              'h1' => array( t('Heading'), '<h1>' . t('Title') . '</h1>'),
              'h2' => array( t('Heading'), '<h2>' . t('Subtitle') . '</h2>'),
              'h3' => array( t('Heading'), '<h3>' . t('Subtitle three') . '</h3>'),
              'h4' => array( t('Heading'), '<h4>' . t('Subtitle four') . '</h4>'),
              'h5' => array( t('Heading'), '<h5>' . t('Subtitle five') . '</h5>'),
              'h6' => array( t('Heading'), '<h6>' . t('Subtitle six') . '</h6>')
220 221 222 223 224 225
            );
            $header = array(t('Tag Description'), t('You Type'), t('You Get'));
            preg_match_all('/<([a-z0-9]+)[^a-z0-9]/i', $allowed_html, $out);
            foreach ($out[1] as $tag) {
              if (array_key_exists($tag, $tips)) {
                if ($tips[$tag]) {
Dries's avatar
Dries committed
226
                  $rows[] = array(
227
                    array('data' => $tips[$tag][0], 'class' => 'description'),
228
                    array('data' => '<code>' . check_plain($tips[$tag][1]) . '</code>', 'class' => 'type'),
229
                    array('data' => $tips[$tag][1], 'class' => 'get')
Dries's avatar
Dries committed
230 231 232
                  );
                }
              }
233
              else {
Dries's avatar
Dries committed
234
                $rows[] = array(
235
                  array('data' => t('No help provided for tag %tag.', array('%tag' => $tag)), 'class' => 'description', 'colspan' => 3),
236
                );
Dries's avatar
Dries committed
237
              }
238 239 240
            }
            $output .= theme('table', $header, $rows);

241 242
            $output .= '<p>' . t('Most unusual characters can be directly entered without any problems.') . '</p>';
            $output .= '<p>' . t('If you do encounter problems, try using HTML character entities. A common example looks like &amp;amp; for an ampersand &amp; character. For a full list of entities see HTML\'s <a href="@html-entities">entities</a> page. Some of the available characters include:', array('@html-entities' => 'http://www.w3.org/TR/html4/sgml/entities.html')) . '</p>';
243 244 245 246 247 248 249 250 251 252 253 254

            $entities = array(
              array( t('Ampersand'), '&amp;'),
              array( t('Greater than'), '&gt;'),
              array( t('Less than'), '&lt;'),
              array( t('Quotation mark'), '&quot;'),
            );
            $header = array(t('Character Description'), t('You Type'), t('You Get'));
            unset($rows);
            foreach ($entities as $entity) {
              $rows[] = array(
                array('data' => $entity[0], 'class' => 'description'),
255
                array('data' => '<code>' . check_plain($entity[1]) . '</code>', 'class' => 'type'),
256 257 258 259 260
                array('data' => $entity[1], 'class' => 'get')
              );
            }
            $output .= theme('table', $header, $rows);
            return $output;
Dries's avatar
Dries committed
261
        }
Dries's avatar
 
Dries committed
262 263
      }
      break;
264 265

    case 1:
266 267 268 269 270 271
      switch ($long) {
        case 0:
          return t('Lines and paragraphs break automatically.');
        case 1:
          return t('Lines and paragraphs are automatically recognized. The &lt;br /&gt; line break, &lt;p&gt; paragraph and &lt;/p&gt; close paragraph tags are inserted automatically. If paragraphs are not recognized simply add a couple blank lines.');
      }
272
      break;
273

274
    case 2:
275
      return t('Web page addresses and e-mail addresses turn into links automatically.');
276 277 278 279 280 281
      break;

    case 4:
      return t('No HTML tags allowed');
      break;

282 283 284
  }
}

Dries's avatar
Dries committed
285
/**
286
 * Retrieve a list of input formats.
Dries's avatar
Dries committed
287
 */
288
function filter_formats($index = NULL) {
289 290 291 292 293 294 295 296 297
  global $user;
  static $formats;

  // Administrators can always use all input formats.
  $all = user_access('administer filters');

  if (!isset($formats)) {
    $formats = array();

298
    $query = db_select('filter_format', 'f');
299 300 301 302 303 304
    $query->addField('f', 'format', 'format');
    $query->addField('f', 'name', 'name');
    $query->addField('f', 'roles', 'roles');
    $query->addField('f', 'cache', 'cache');
    $query->addField('f', 'weight', 'weight');
    $query->orderBy('weight');
305

306
    // Build query for selecting the format(s) based on the user's roles.
307
    if (!$all) {
308
      $or = db_or()->condition('format', variable_get('filter_default_format', 1));
309
      foreach ($user->roles as $rid => $role) {
310
        $or->condition('roles', '%'. (int)$rid .'%', 'LIKE');
311
      }
312
      $query->condition($or);
313 314
    }

315
    $formats = $query->execute()->fetchAllAssoc('format');
316
  }
317 318 319
  if (isset($index)) {
    return isset($formats[$index]) ? $formats[$index] : FALSE;
  }
320 321
  return $formats;
}
322

323 324 325 326 327 328
/**
 * Build a list of all filters.
 */
function filter_list_all() {
  $filters = array();

329 330 331
  foreach (module_implements('filter') as $module) {
    $function = $module . '_filter';
    $list = $function('list');
332
    if (isset($list) && is_array($list)) {
333
      foreach ($list as $delta => $name) {
334
        $filters[$module . '/' . $delta] = (object)array('module' => $module, 'delta' => $delta, 'name' => $name);
335
      }
Dries's avatar
 
Dries committed
336 337 338
    }
  }

339 340 341 342 343 344 345 346 347 348
  uasort($filters, '_filter_list_cmp');

  return $filters;
}

/**
 * Helper function for sorting the filter list by filter name.
 */
function _filter_list_cmp($a, $b) {
  return strcmp($a->name, $b->name);
Dries's avatar
 
Dries committed
349 350
}

351 352 353 354 355 356
/**
 * Resolve a format id, including the default format.
 */
function filter_resolve_format($format) {
  return $format == FILTER_FORMAT_DEFAULT ? variable_get('filter_default_format', 1) : $format;
}
Dries's avatar
Dries committed
357
/**
358
 * Check if text in a certain input format is allowed to be cached.
Dries's avatar
Dries committed
359
 */
360 361
function filter_format_allowcache($format) {
  static $cache = array();
362
  $format = filter_resolve_format($format);
363
  if (!isset($cache[$format])) {
364
    $cache[$format] = db_result(db_query('SELECT cache FROM {filter_format} WHERE format = %d', $format));
365 366 367 368 369 370 371 372 373 374
  }
  return $cache[$format];
}

/**
 * Retrieve a list of filters for a certain format.
 */
function filter_list_format($format) {
  static $filters = array();

375
  if (!isset($filters[$format])) {
376
    $filters[$format] = array();
377
    $result = db_query("SELECT * FROM {filter} WHERE format = %d ORDER BY weight, module, delta", $format);
378 379
    while ($filter = db_fetch_object($result)) {
      $list = module_invoke($filter->module, 'filter', 'list');
380
      if (isset($list) && is_array($list) && isset($list[$filter->delta])) {
381
        $filter->name = $list[$filter->delta];
382
        $filters[$format][$filter->module . '/' . $filter->delta] = $filter;
Dries's avatar
 
Dries committed
383 384 385 386
      }
    }
  }

387
  return $filters[$format];
388 389
}

390 391
/**
 * @name Filtering functions
Dries's avatar
 
Dries committed
392
 * @{
393 394
 * Modules which need to have content filtered can use these functions to
 * interact with the filter system.
395 396 397 398 399 400 401 402
 *
 * For more info, see the hook_filter() documentation.
 *
 * Note: because filters can inject JavaScript or execute PHP code, security is
 * vital here. When a user supplies a $format, you should validate it with
 * filter_access($format) before accepting/using it. This is normally done in
 * the validation stage of the node system. You should for example never make a
 * preview of content in a disallowed format.
403 404
 */

Dries's avatar
Dries committed
405 406
/**
 * Run all the enabled filters on a piece of text.
407 408 409 410 411 412
 *
 * @param $text
 *    The text to be filtered.
 * @param $format
 *    The format of the text to be filtered. Specify FILTER_FORMAT_DEFAULT for
 *    the default format.
413 414 415 416
 * @param $langcode
 *    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.
417 418 419
 * @param $check
 *    Whether to check the $format with filter_access() first. Defaults to TRUE.
 *    Note that this will check the permissions of the current user, so you
420
 *    should specify $check = FALSE when viewing other people's content. When
421 422
 *    showing content that is not (yet) stored in the database (eg. upon preview),
 *    set to TRUE so the user's permissions are checked.
Dries's avatar
Dries committed
423
 */
424
function check_markup($text, $format = FILTER_FORMAT_DEFAULT, $langcode = '', $check = TRUE) {
425
  // When $check = TRUE, do an access check on $format.
426
  if (isset($text) && (!$check || filter_access($format))) {
427
    $format = filter_resolve_format($format);
428

429
    // Check for a cached version of this piece of text.
430
    $cache_id = $format . ':' . $langcode . ':' . md5($text);
431
    if ($cached = cache_get($cache_id, 'cache_filter')) {
432 433 434
      return $cached->data;
    }

435
    // See if caching is allowed for this format.
436
    $cache = filter_format_allowcache($format);
437 438

    // Convert all Windows and Mac newlines to a single newline,
439
    // so filters only need to deal with one possibility.
440 441
    $text = str_replace(array("\r\n", "\r"), "\n", $text);

442
    // Get a complete list of filters, ordered properly.
443
    $filters = filter_list_format($format);
Dries's avatar
 
Dries committed
444

Dries's avatar
Dries committed
445
    // Give filters the chance to escape HTML-like data such as code or formulas.
446
    foreach ($filters as $filter) {
447
      $text = module_invoke($filter->module, 'filter', 'prepare', $filter->delta, $format, $text, $langcode, $cache_id);
Dries's avatar
 
Dries committed
448
    }
449

450
    // Perform filtering.
451
    foreach ($filters as $filter) {
452
      $text = module_invoke($filter->module, 'filter', 'process', $filter->delta, $format, $text, $langcode, $cache_id);
453 454
    }

455
    // Store in cache with a minimum expiration time of 1 day.
Dries's avatar
Dries committed
456
    if ($cache) {
457
      cache_set($cache_id, $text, 'cache_filter', REQUEST_TIME + (60 * 60 * 24));
Dries's avatar
Dries committed
458 459 460
    }
  }
  else {
461
    $text = t('n/a');
Dries's avatar
Dries committed
462 463 464 465 466 467 468 469
  }

  return $text;
}

/**
 * Generate a selector for choosing a format in a form.
 *
470
 * @ingroup forms
471
 * @see filter_form_validate()
Dries's avatar
Dries committed
472 473
 * @param $value
 *   The ID of the format that is currently selected.
474 475 476 477
 * @param $weight
 *   The weight of the input format.
 * @param $parents
 *   Required when defining multiple input formats on a single node or having a different parent than 'format'.
Dries's avatar
Dries committed
478 479 480
 * @return
 *   HTML for the form element.
 */
481
function filter_form($value = FILTER_FORMAT_DEFAULT, $weight = NULL, $parents = array('format')) {
Steven Wittens's avatar
Oopsie  
Steven Wittens committed
482
  $value = filter_resolve_format($value);
Dries's avatar
Dries committed
483 484
  $formats = filter_formats();

485
  $extra = theme('filter_tips_more_info');
Dries's avatar
Dries committed
486 487

  if (count($formats) > 1) {
488
    $form = array(
489 490 491 492 493
      '#type' => 'fieldset',
      '#title' => t('Input format'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#weight' => $weight,
494
      '#element_validate' => array('filter_form_validate'),
495
    );
Dries's avatar
Dries committed
496 497
    // Multiple formats available: display radio buttons with tips.
    foreach ($formats as $format) {
498 499 500
      // Generate the parents as the autogenerator does, so we will have a
      // unique id for each radio button.
      $parents_for_id = array_merge($parents, array($format->format));
501
      $form[$format->format] = array(
502
        '#type' => 'radio',
503 504 505
        '#title' => $format->name,
        '#default_value' => $value,
        '#return_value' => $format->format,
506
        '#parents' => $parents,
507
        '#description' => theme('filter_tips', _filter_tips($format->format, FALSE)),
508
        '#id' => form_clean_id('edit-' . implode('-', $parents_for_id)),
509
      );
Dries's avatar
Dries committed
510 511 512 513 514
    }
  }
  else {
    // Only one format available: use a hidden form item and only show tips.
    $format = array_shift($formats);
515
    $form[$format->format] = array('#type' => 'value', '#value' => $format->format, '#parents' => $parents);
516
    $tips = _filter_tips(variable_get('filter_default_format', 1), FALSE);
517
    $form['format']['guidelines'] = array(
518
      '#title' => t('Formatting guidelines'),
519
      '#markup' => theme('filter_tips', $tips, FALSE, $extra),
520
    );
Dries's avatar
Dries committed
521
  }
522
  $form[] = array('#markup' => $extra);
523
  return $form;
Dries's avatar
Dries committed
524 525
}

526 527 528 529 530 531 532
function filter_form_validate($form) {
  foreach (element_children($form) as $key) {
    if ($form[$key]['#value'] == $form[$key]['#return_value']) {
      return;
    }
  }
  form_error($form, t('An illegal choice has been detected. Please contact the site administrator.'));
533
  watchdog('form', 'Illegal choice %choice in %name element.', array('%choice' => $form[$key]['#value'], '%name' => empty($form['#title']) ? $form['#parents'][0] : $form['#title']), WATCHDOG_ERROR);
534 535
}

Dries's avatar
Dries committed
536
/**
537
 * Returns TRUE if the user is allowed to access this format.
Dries's avatar
Dries committed
538 539
 */
function filter_access($format) {
540 541
  $format = filter_resolve_format($format);
  if (user_access('administer filters') || ($format == variable_get('filter_default_format', 1))) {
542
    return TRUE;
Dries's avatar
Dries committed
543 544 545 546 547 548
  }
  else {
    $formats = filter_formats();
    return isset($formats[$format]);
  }
}
549

Dries's avatar
Dries committed
550 551 552 553 554 555 556 557
/**
 * @} End of "Filtering functions".
 */


/**
 * Helper function for fetching filter tips.
 */
558
function _filter_tips($format, $long = FALSE) {
Dries's avatar
Dries committed
559 560 561 562
  if ($format == -1) {
    $formats = filter_formats();
  }
  else {
563
    $formats = array(db_fetch_object(db_query("SELECT * FROM {filter_format} WHERE format = %d", $format)));
Dries's avatar
Dries committed
564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
  }

  $tips = array();

  foreach ($formats as $format) {
    $filters = filter_list_format($format->format);

    $tips[$format->name] = array();
    foreach ($filters as $id => $filter) {
      if ($tip = module_invoke($filter->module, 'filter_tips', $filter->delta, $format->format, $long)) {
        $tips[$format->name][] = array('tip' => $tip, 'id' => $id);
      }
    }
  }

  return $tips;
}


583 584 585 586 587 588
/**
 * Format a link to the more extensive filter tips.
 *
 * @ingroup themeable
 */
function theme_filter_tips_more_info() {
589
  return '<p>' . l(t('More information about formatting options'), 'filter/tips') . '</p>';
590 591
}

Dries's avatar
Dries committed
592 593 594 595 596 597 598 599 600 601 602 603
/**
 * @name Standard filters
 * @{
 * Filters implemented by the filter.module.
 */

/**
 * Implementation of hook_filter(). Contains a basic set of essential filters.
 * - HTML filter:
 *     Validates user-supplied HTML, transforming it as necessary.
 * - Line break converter:
 *     Converts newlines into paragraph and break tags.
604 605
 * - URL and e-mail address filter:
 *     Converts newlines into paragraph and break tags.
Dries's avatar
Dries committed
606 607 608 609
 */
function filter_filter($op, $delta = 0, $format = -1, $text = '') {
  switch ($op) {
    case 'list':
610
      return array(0 => t('Limit allowed HTML tags'), 1 => t('Convert line breaks'), 2 => t('Convert URLs into links'), 3 => t('Correct broken HTML'), 4 => t('Escape all HTML'));
Dries's avatar
Dries committed
611 612 613 614

    case 'description':
      switch ($delta) {
        case 0:
615
          return t('Allows you to restrict the HTML tags the user can use. It will also remove harmful content such as JavaScript events, JavaScript URLs and CSS styles from those tags that are not removed.');
Dries's avatar
Dries committed
616
        case 1:
617
          return t('Converts line breaks into HTML (i.e. &lt;br&gt; and &lt;p&gt;) tags.');
618
        case 2:
619
          return t('Turns web and e-mail addresses into clickable links.');
620 621
        case 3:
          return t('Corrects faulty and chopped off HTML in postings.');
622 623
        case 4:
          return t('Escapes all HTML tags, so they will be visible instead of being effective.');
Dries's avatar
Dries committed
624 625 626 627 628 629 630 631 632 633
        default:
          return;
      }

    case 'process':
      switch ($delta) {
        case 0:
          return _filter_html($text, $format);
        case 1:
          return _filter_autop($text);
634
        case 2:
635
          return _filter_url($text, $format);
636 637
        case 3:
          return _filter_htmlcorrector($text);
638 639
        case 4:
          return trim(check_plain($text));
Dries's avatar
Dries committed
640 641 642 643 644 645 646 647
        default:
          return $text;
      }

    case 'settings':
      switch ($delta) {
        case 0:
          return _filter_html_settings($format);
648
        case 2:
649
          return _filter_url_settings($format);
Dries's avatar
Dries committed
650 651 652 653 654 655 656 657 658 659 660 661 662
        default:
          return;
      }

    default:
      return $text;
  }
}

/**
 * Settings for the HTML filter.
 */
function _filter_html_settings($format) {
Dries's avatar
-Patch  
Dries committed
663 664 665 666 667 668 669 670
  $form['filter_html'] = array(
    '#type' => 'fieldset',
    '#title' => t('HTML filter'),
    '#collapsible' => TRUE,
  );
  $form['filter_html']["allowed_html_$format"] = array(
    '#type' => 'textfield',
    '#title' => t('Allowed HTML tags'),
671
    '#default_value' => variable_get("allowed_html_$format", '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>'),
Dries's avatar
-Patch  
Dries committed
672
    '#size' => 64,
673
    '#maxlength' => 1024,
674
    '#description' => t('Specify a list of tags which should not be stripped. (Note that JavaScript event attributes are always stripped.)'),
Dries's avatar
-Patch  
Dries committed
675 676 677 678 679 680 681 682 683 684 685 686 687
  );
  $form['filter_html']["filter_html_help_$format"] = array(
    '#type' => 'checkbox',
    '#title' => t('Display HTML help'),
    '#default_value' => variable_get("filter_html_help_$format", 1),
    '#description' => t('If enabled, Drupal will display some basic HTML help in the long filter tips.'),
  );
  $form['filter_html']["filter_html_nofollow_$format"] = array(
    '#type' => 'checkbox',
    '#title' => t('Spam link deterrent'),
    '#default_value' => variable_get("filter_html_nofollow_$format", FALSE),
    '#description' => t('If enabled, Drupal will add rel="nofollow" to all links, as a measure to reduce the effectiveness of spam links. Note: this will also prevent valid links from being followed by search engines, therefore it is likely most effective when enabled for anonymous users.'),
  );
688
  return $form;
Dries's avatar
Dries committed
689 690 691 692 693 694
}

/**
 * HTML filter. Provides filtering of input into accepted HTML.
 */
function _filter_html($text, $format) {
695
  $allowed_tags = preg_split('/\s+|<|>/', variable_get("allowed_html_$format", '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>'), -1, PREG_SPLIT_NO_EMPTY);
696
  $text = filter_xss($text, $allowed_tags);
Dries's avatar
Dries committed
697 698 699 700 701 702 703 704

  if (variable_get("filter_html_nofollow_$format", FALSE)) {
    $text = preg_replace('/<a([^>]+)>/i', '<a\\1 rel="nofollow">', $text);
  }

  return trim($text);
}

705 706 707 708 709 710 711 712 713
/**
 * Settings for URL filter.
 */
function _filter_url_settings($format) {
  $form['filter_urlfilter'] = array(
    '#type' => 'fieldset',
    '#title' => t('URL filter'),
    '#collapsible' => TRUE,
  );
714
  $form['filter_urlfilter']['filter_url_length_' . $format] = array(
715 716
    '#type' => 'textfield',
    '#title' => t('Maximum link text length'),
717
    '#default_value' => variable_get('filter_url_length_' . $format, 72),
718
    '#maxlength' => 4,
drumm's avatar
drumm committed
719
    '#description' => t('URLs longer than this number of characters will be truncated to prevent long strings that break formatting. The link itself will be retained; just the text portion of the link will be truncated.'),
720 721 722 723 724 725 726 727 728 729
  );
  return $form;
}

/**
 * URL filter. Automatically converts text web addresses (URLs, e-mail addresses,
 * ftp links, etc.) into hyperlinks.
 */
function _filter_url($text, $format) {
  // Pass length to regexp callback
730
  _filter_url_trim(NULL, variable_get('filter_url_length_' . $format, 72));
731

732
  $text = ' ' . $text . ' ';
733 734

  // Match absolute URLs.
735
  $text = preg_replace_callback("`(<p>|<li>|<br\s*/?>|[ \n\r\t\(])((http://|https://|ftp://|mailto:|smb://|afp://|file://|gopher://|news://|ssl://|sslv2://|sslv3://|tls://|tcp://|udp://)([a-zA-Z0-9@:%_+*~#?&=.,/;-]*[a-zA-Z0-9@:%_+*~#&=/;-]))([.,?!]*?)(?=(</p>|</li>|<br\s*/?>|[ \n\r\t\)]))`i", '_filter_url_parse_full_links', $text);
736

737
  // Match e-mail addresses.
738
  $text = preg_replace("`(<p>|<li>|<br\s*/?>|[ \n\r\t\(])([A-Za-z0-9._-]+@[A-Za-z0-9._+-]+\.[A-Za-z]{2,4})([.,?!]*?)(?=(</p>|</li>|<br\s*/?>|[ \n\r\t\)]))`i", '\1<a href="mailto:\2">\2</a>\3', $text);
739 740

  // Match www domains/addresses.
741
  $text = preg_replace_callback("`(<p>|<li>|[ \n\r\t\(])(www\.[a-zA-Z0-9@:%_+*~#?&=.,/;-]*[a-zA-Z0-9@:%_+~#\&=/;-])([.,?!]*?)(?=(</p>|</li>|<br\s*/?>|[ \n\r\t\)]))`i", '_filter_url_parse_partial_links', $text);
742 743 744 745 746
  $text = substr($text, 1, -1);

  return $text;
}

747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768
/**
 * Scan input and make sure that all HTML tags are properly closed and nested.
 */
function _filter_htmlcorrector($text) {
  // Prepare tag lists.
  static $no_nesting, $single_use;
  if (!isset($no_nesting)) {
    // Tags which cannot be nested but are typically left unclosed.
    $no_nesting = drupal_map_assoc(array('li', 'p'));

    // Single use tags in HTML4
    $single_use = drupal_map_assoc(array('base', 'meta', 'link', 'hr', 'br', 'param', 'img', 'area', 'input', 'col', 'frame'));
  }

  // Properly entify angles.
  $text = preg_replace('!<([^a-zA-Z/])!', '&lt;\1', $text);

  // Split tags from text.
  $split = preg_split('/<([^>]+?)>/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
  // Note: PHP ensures the array consists of alternating delimiters and literals
  // and begins and ends with a literal (inserting $null as required).

769
  $tag = FALSE; // Odd/even counter. Tag or no tag.
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784
  $stack = array();
  $output = '';
  foreach ($split as $value) {
    // Process HTML tags.
    if ($tag) {
      list($tagname) = explode(' ', strtolower($value), 2);
      // Closing tag
      if ($tagname{0} == '/') {
        $tagname = substr($tagname, 1);
        // Discard XHTML closing tags for single use tags.
        if (!isset($single_use[$tagname])) {
          // See if we possibly have a matching opening tag on the stack.
          if (in_array($tagname, $stack)) {
            // Close other tags lingering first.
            do {
785
              $output .= '</' . $stack[0] . '>';
786 787 788 789 790 791 792 793 794
            } while (array_shift($stack) != $tagname);
          }
          // Otherwise, discard it.
        }
      }
      // Opening tag
      else {
        // See if we have an identical 'no nesting' tag already open and close it if found.
        if (count($stack) && ($stack[0] == $tagname) && isset($no_nesting[$stack[0]])) {
795
          $output .= '</' . array_shift($stack) . '>';
796 797 798 799 800 801 802
        }
        // Push non-single-use tags onto the stack
        if (!isset($single_use[$tagname])) {
          array_unshift($stack, $tagname);
        }
        // Add trailing slash to single-use tags as per X(HT)ML.
        else {
803
          $value = rtrim($value, ' /') . ' /';
804
        }
805
        $output .= '<' . $value . '>';
806 807 808 809 810 811 812 813 814 815
      }
    }
    else {
      // Passthrough all text.
      $output .= $value;
    }
    $tag = !$tag;
  }
  // Close remaining tags.
  while (count($stack) > 0) {
816
    $output .= '</' . array_shift($stack) . '>';
817 818 819 820
  }
  return $output;
}

821
/**
822
 * Make links out of absolute URLs.
823 824 825 826 827
 */
function _filter_url_parse_full_links($match) {
  $match[2] = decode_entities($match[2]);
  $caption = check_plain(_filter_url_trim($match[2]));
  $match[2] = check_url($match[2]);
828
  return $match[1] . '<a href="' . $match[2] . '" title="' . $match[2] . '">' . $caption . '</a>' . $match[5];
829 830 831 832 833 834 835 836 837
}

/**
 * Make links out of domain names starting with "www."
 */
function _filter_url_parse_partial_links($match) {
  $match[2] = decode_entities($match[2]);
  $caption = check_plain(_filter_url_trim($match[2]));
  $match[2] = check_plain($match[2]);
838
  return $match[1] . '<a href="http://' . $match[2] . '" title="' . $match[2] . '">' . $caption . '</a>' . $match[3];
839 840 841 842 843 844 845 846 847 848 849
}

/**
 * Shortens long URLs to http://www.example.com/long/url...
 */
function _filter_url_trim($text, $length = NULL) {
  static $_length;
  if ($length !== NULL) {
    $_length = $length;
  }

850 851
  // Use +3 for '...' string length.
  if (strlen($text) > $_length + 3) {
852
    $text = substr($text, 0, $_length) . '...';
853 854 855 856 857
  }

  return $text;
}

Dries's avatar
Dries committed
858 859 860 861 862
/**
 * Convert line breaks into <p> and <br> in an intelligent fashion.
 * Based on: http://photomatt.net/scripts/autop
 */
function _filter_autop($text) {
863
  // All block level tags
864
  $block = '(?:table|thead|tfoot|caption|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|form|blockquote|address|p|h[1-6]|hr)';
Dries's avatar
Dries committed
865

Dries's avatar
Dries committed
866 867 868 869
  // Split at <pre>, <script>, <style> and </pre>, </script>, </style> tags.
  // We don't apply any processing to the contents of these tags to avoid messing
  // up code. We look for matched pairs and allow basic nesting. For example:
  // "processed <pre> ignored <script> ignored </script> ignored </pre> processed"
870
  $chunks = preg_split('@(</?(?:pre|script|style|object)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
Dries's avatar
Dries committed
871 872
  // Note: PHP ensures the array consists of alternating delimiters and literals
  // and begins and ends with a literal (inserting NULL as required).
873
  $ignore = FALSE;
Dries's avatar
Dries committed
874 875 876 877 878
  $ignoretag = '';
  $output = '';
  foreach ($chunks as $i => $chunk) {
    if ($i % 2) {
      // Opening or closing tag?
879
      $open = ($chunk[1] != '/');
880
      list($tag) = preg_split('/[ >]/', substr($chunk, 2 - $open), 2);
Dries's avatar
Dries committed
881 882
      if (!$ignore) {
        if ($open) {
883
          $ignore = TRUE;
Dries's avatar
Dries committed
884 885 886 887
          $ignoretag = $tag;
        }
      }
      // Only allow a matching tag to close it.
888
      elseif (!$open && $ignoretag == $tag) {
889
        $ignore = FALSE;
Dries's avatar
Dries committed
890 891 892
        $ignoretag = '';
      }
    }
893
    elseif (!$ignore) {