Simpletest Coverage - modules/filter/filter.module

1 <?php
2 // $Id: filter.module,v 1.269 2009/08/15 06:45:31 webchick Exp $
3
4 /**
5 * @file
6 * Framework for handling filtering of content.
7 */
8
9 /**
10 * Special format ID which means "use the default format".
11 *
12 * This value can be passed to the filter APIs as a format ID: this is
13 * equivalent to not passing an explicit format at all.
14 */
15 define('FILTER_FORMAT_DEFAULT', 0);
16
17 /**
18 * Implement hook_help().
19 */
20 function filter_help($path, $arg) {
21 switch ($path) {
22 case 'admin/help#filter':
23 $output = '<p>' . t("The filter module allows administrators to configure text formats for use on your site. A text 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 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 formats may be created by an administrator.") . '</p>';
24 $output .= '<p>' . 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 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>';
25 $output .= '<p>' . 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 determine the text formats available to each user role, select a default text format, and control the order of formats listed in the <em>Text format</em> fieldset.') . '</p>';
26 $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/formats':
29 $output = '<p>' . t('Use the list below to review the text formats available to each user role, to select a default text format, and to control the order of formats listed in the <em>Text format</em> fieldset. (The <em>Text format</em> fieldset is displayed below textareas when users with access to more than one text format create multi-line content.) The text format selected as <em>Default</em> is available to all users and, unless another format is selected, is applied to all content. All text formats are available to users in roles with the "administer filters" permission.') . '</p>';
30 $output .= '<p>' . t('Since text 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. 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/formats/%':
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 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/formats/' . $arg[3] . '/order'))) . '</p>';
34 case 'admin/settings/formats/%/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/formats/' . $arg[3]))) . '</p>';
36 case 'admin/settings/formats/%/order':
37 $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>';
38 $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 * Implement hook_theme().
45 */
46 function filter_theme() {
47 return array(
48 'filter_admin_overview' => array(
49 'arguments' => array('form' => NULL),
50 'file' => 'filter.admin.inc',
51 ),
52 'filter_admin_order' => array(
53 'arguments' => array('form' => NULL),
54 'file' => 'filter.admin.inc',
55 ),
56 'filter_tips' => array(
57 'arguments' => array('tips' => NULL, 'long' => FALSE),
58 'file' => 'filter.pages.inc',
59 ),
60 'filter_tips_more_info' => array(
61 'arguments' => array(),
62 ),
63 'filter_guidelines' => array(
64 'arguments' => array('format' => NULL),
65 ),
66 );
67 }
68
69 /**
70 * Implement hook_menu().
71 */
72 function filter_menu() {
73 $items['admin/settings/formats'] = array(
74 'title' => 'Text formats',
75 'description' => 'Configure how content input by users is filtered, including allowed HTML tags. Also allows enabling of module-provided filters.',
76 'page callback' => 'drupal_get_form',
77 'page arguments' => array('filter_admin_overview'),
78 'access arguments' => array('administer filters'),
79 );
80 $items['admin/settings/formats/list'] = array(
81 'title' => 'List',
82 'type' => MENU_DEFAULT_LOCAL_TASK,
83 );
84 $items['admin/settings/formats/add'] = array(
85 'title' => 'Add text format',
86 'page callback' => 'filter_admin_format_page',
87 'access arguments' => array('administer filters'),
88 'type' => MENU_LOCAL_TASK,
89 'weight' => 1,
90 );
91 $items['admin/settings/formats/delete'] = array(
92 'title' => 'Delete text format',
93 'page callback' => 'drupal_get_form',
94 'page arguments' => array('filter_admin_delete'),
95 'access arguments' => array('administer filters'),
96 'type' => MENU_CALLBACK,
97 );
98 $items['filter/tips'] = array(
99 'title' => 'Compose tips',
100 'page callback' => 'filter_tips_long',
101 'access callback' => TRUE,
102 'type' => MENU_SUGGESTED_ITEM,
103 );
104 $items['admin/settings/formats/%filter_format'] = array(
105 'type' => MENU_CALLBACK,
106 'title callback' => 'filter_admin_format_title',
107 'title arguments' => array(3),
108 'page callback' => 'filter_admin_format_page',
109 'page arguments' => array(3),
110 'access arguments' => array('administer filters'),
111 );
112 $items['admin/settings/formats/%filter_format/edit'] = array(
113 'title' => 'Edit',
114 'type' => MENU_DEFAULT_LOCAL_TASK,
115 'weight' => 0,
116 );
117 $items['admin/settings/formats/%filter_format/configure'] = array(
118 'title' => 'Configure',
119 'page callback' => 'filter_admin_configure_page',
120 'page arguments' => array(3),
121 'access arguments' => array('administer filters'),
122 'type' => MENU_LOCAL_TASK,
123 'weight' => 1,
124 );
125 $items['admin/settings/formats/%filter_format/order'] = array(
126 'title' => 'Rearrange',
127 'page callback' => 'filter_admin_order_page',
128 'page arguments' => array(3),
129 'access arguments' => array('administer filters'),
130 'type' => MENU_LOCAL_TASK,
131 'weight' => 2,
132 );
133 return $items;
134 }
135
136 function filter_format_load($arg) {
137 return filter_formats($arg);
138 }
139
140 /**
141 * Display a text format form title.
142 */
143 function filter_admin_format_title($format) {
144 return $format->name;
145 }
146
147 /**
148 * Implement hook_permission().
149 */
150 function filter_permission() {
151 return array(
152 'administer filters' => array(
153 'title' => t('Administer filters'),
154 'description' => t('Manage text 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.'))),
155 ),
156 );
157 }
158
159 /**
160 * Implement hook_cron().
161 *
162 * Expire outdated filter cache entries
163 */
164 function filter_cron() {
165 cache_clear_all(NULL, 'cache_filter');
166 }
167
168 /**
169 * @name Tips callbacks for filters.
170 * @{
171 * Filters implemented by the filter.module.
172 */
173 function _filter_html_tips($format, $long = FALSE) {
174 global $base_url;
175 if ($allowed_html = variable_get("allowed_html_$format", '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>')) {
176 if ($long) {
177 $output = '<p>' . t('Allowed HTML tags: @tags', array('@tags' => $allowed_html)) . '</p>';
178 if (!variable_get("filter_html_help_$format", 1)) {
179 return $output;
180 }
181
182 $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>';
183 $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 '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>'),
188 'strong' => array( t('Strong'), '<strong>' . t('Strong') . '</strong>'),
189 'em' => array( t('Emphasized'), '<em>' . t('Emphasized') . '</em>'),
190 'cite' => array( t('Cited'), '<cite>' . t('Cited') . '</cite>'),
191 'code' => array( t('Coded text used to show programming source code'), '<code>' . t('Coded') . '</code>'),
192 'b' => array( t('Bolded'), '<b>' . t('Bolded') . '</b>'),
193 'u' => array( t('Underlined'), '<u>' . t('Underlined') . '</u>'),
194 'i' => array( t('Italicized'), '<i>' . t('Italicized') . '</i>'),
195 'sup' => array( t('Superscripted'), t('<sup>Super</sup>scripted')),
196 'sub' => array( t('Subscripted'), t('<sub>Sub</sub>scripted')),
197 'pre' => array( t('Preformatted'), '<pre>' . t('Preformatted') . '</pre>'),
198 'abbr' => array( t('Abbreviation'), t('<abbr title="Abbreviation">Abbrev.</abbr>')),
199 'acronym' => array( t('Acronym'), t('<acronym title="Three-Letter Acronym">TLA</acronym>')),
200 'blockquote' => array( t('Block quoted'), '<blockquote>' . t('Block quoted') . '</blockquote>'),
201 '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 'del' => array( t('Deleted'), '<del>' . t('Deleted') . '</del>'),
206 'ins' => array( t('Inserted'), '<ins>' . t('Inserted') . '</ins>'),
207 // Assumes and describes li.
208 '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>'),
209 '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 'li' => NULL,
211 // 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 'h1' => array( t('Heading'), '<h1>' . t('Title') . '</h1>'),
215 'h2' => array( t('Heading'), '<h2>' . t('Subtitle') . '</h2>'),
216 'h3' => array( t('Heading'), '<h3>' . t('Subtitle three') . '</h3>'),
217 'h4' => array( t('Heading'), '<h4>' . t('Subtitle four') . '</h4>'),
218 'h5' => array( t('Heading'), '<h5>' . t('Subtitle five') . '</h5>'),
219 'h6' => array( t('Heading'), '<h6>' . t('Subtitle six') . '</h6>')
220 );
221 $header = array(t('Tag Description'), t('You Type'), t('You Get'));
222 preg_match_all('/<([a-z0-9]+)[^a-z0-9]/i', $allowed_html, $out);
223 foreach ($out[1] as $tag) {
224 if (array_key_exists($tag, $tips)) {
225 if ($tips[$tag]) {
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')
230 );
231 }
232 }
233 else {
234 $rows[] = array(
235 array('data' => t('No help provided for tag %tag.', array('%tag' => $tag)), 'class' => 'description', 'colspan' => 3),
236 );
237 }
238 }
239 $output .= theme('table', $header, $rows);
240
241 $output .= '<p>' . t('Most unusual characters can be directly entered without any problems.') . '</p>';
242 $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 $entities = array(
245 array( t('Ampersand'), '&amp;'),
246 array( t('Greater than'), '&gt;'),
247 array( t('Less than'), '&lt;'),
248 array( t('Quotation mark'), '&quot;'),
249 );
250 $header = array(t('Character Description'), t('You Type'), t('You Get'));
251 unset($rows);
252 foreach ($entities as $entity) {
253 $rows[] = array(
254 array('data' => $entity[0], 'class' => 'description'),
255 array('data' => '<code>' . check_plain($entity[1]) . '</code>', 'class' => 'type'),
256 array('data' => $entity[1], 'class' => 'get')
257 );
258 }
259 $output .= theme('table', $header, $rows);
260 return $output;
261 }
262
263 else {
264 return t('Allowed HTML tags: @tags', array('@tags' => $allowed_html));
265 }
266 }
267 }
268
269 function _filter_autop_tips($format, $long = FALSE) {
270 if ($long) {
271 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 }
273 else {
274 return t('Lines and paragraphs break automatically.');
275 }
276 }
277
278 function _filter_url_tips() {
279 return t('Web page addresses and e-mail addresses turn into links automatically.');
280 }
281
282 function _filter_html_escape_tips() {
283 return t('No HTML tags allowed.');
284 }
285
286 /**
287 * @} End of "Tips callback for filters".
288 */
289 /**
290 * Retrieve a list of text formats.
291 */
292 function filter_formats($index = NULL) {
293 global $user;
294 static $formats;
295
296 // Administrators can always use all text formats.
297 $all = user_access('administer filters');
298
299 if (!isset($formats)) {
300 $formats = array();
301
302 $query = db_select('filter_format', 'f');
303 $query->addField('f', 'format', 'format');
304 $query->addField('f', 'name', 'name');
305 $query->addField('f', 'roles', 'roles');
306 $query->addField('f', 'cache', 'cache');
307 $query->addField('f', 'weight', 'weight');
308 $query->orderBy('weight');
309
310 // Build query for selecting the format(s) based on the user's roles.
311 if (!$all) {
312 $or = db_or()->condition('format', variable_get('filter_default_format', 1));
313 foreach ($user->roles as $rid => $role) {
314 $or->condition('roles', '%' . (int)$rid . '%', 'LIKE');
315 }
316 $query->condition($or);
317 }
318
319 $formats = $query->execute()->fetchAllAssoc('format');
320 }
321 if (isset($index)) {
322 return isset($formats[$index]) ? $formats[$index] : FALSE;
323 }
324 return $formats;
325 }
326
327 /**
328 * Build a list of all filters.
329 */
330 function filter_list_all() {
331 $filters = array();
332
333 foreach (module_implements('filter_info') as $module) {
334 $function = $module . '_filter_info';
335 $info = $function('list');
336 if (isset($info) && is_array($info)) {
337 foreach ($info as $delta => $filter) {
338 $filters[$module . '/' . $delta] = (object)($filter + array(
339 'module' => $module,
340 'delta' => $delta,
341 ));
342 }
343 }
344 }
345
346 uasort($filters, '_filter_list_cmp');
347
348 return $filters;
349 }
350
351 /**
352 * Helper function for sorting the filter list by filter name.
353 */
354 function _filter_list_cmp($a, $b) {
355 return strcmp($a->name, $b->name);
356 }
357
358 /**
359 * Resolve a format id, including the default format.
360 */
361 function filter_resolve_format($format) {
362 return $format == FILTER_FORMAT_DEFAULT ? variable_get('filter_default_format', 1) : $format;
363 }
364 /**
365 * Check if text in a certain text format is allowed to be cached.
366 */
367 function filter_format_allowcache($format) {
368 static $cache = array();
369 $format = filter_resolve_format($format);
370 if (!isset($cache[$format])) {
371 $cache[$format] = db_query('SELECT cache FROM {filter_format} WHERE format = :format', array(':format' => $format))->fetchField();
372 }
373 return $cache[$format];
374 }
375
376 /**
377 * Retrieve a list of filters for a certain format.
378 */
379 function filter_list_format($format) {
380 static $filters = array();
381
382 if (!isset($filters[$format])) {
383 $filters[$format] = array();
384 $result = db_query("SELECT * FROM {filter} WHERE format = :format ORDER BY weight, module, delta", array(':format' => (int) $format));
385 foreach ($result as $filter) {
386 $info = module_invoke($filter->module, 'filter_info');
387 if (isset($info) && is_array($info) && isset($info[$filter->delta])) {
388 $filter->name = $info[$filter->delta]['name'];
389 $filters[$format][$filter->module . '/' . $filter->delta] = $filter;
390 }
391 }
392 }
393
394 return $filters[$format];
395 }
396
397 /**
398 * @name Filtering functions
399 * @{
400 * Modules which need to have content filtered can use these functions to
401 * interact with the filter system.
402 *
403 * For more info, see the hook_filter() documentation.
404 *
405 * Note: because filters can inject JavaScript or execute PHP code, security is
406 * vital here. When a user supplies a $format, you should validate it with
407 * filter_access($format) before accepting/using it. This is normally done in
408 * the validation stage of the node system. You should for example never make a
409 * preview of content in a disallowed format.
410 */
411
412 /**
413 * Run all the enabled filters on a piece of text.
414 *
415 * @param $text
416 * The text to be filtered.
417 * @param $format
418 * The format of the text to be filtered. Specify FILTER_FORMAT_DEFAULT for
419 * the default format.
420 * @param $langcode
421 * Optional: the language code of the text to be filtered, e.g. 'en' for
422 * English. This allows filters to be language aware so language specific
423 * text replacement can be implemented.
424 * @param $check
425 * Whether to check the $format with filter_access() first. Defaults to TRUE.
426 * Note that this will check the permissions of the current user, so you
427 * should specify $check = FALSE when viewing other people's content. When
428 * showing content that is not (yet) stored in the database (eg. upon preview),
429 * set to TRUE so the user's permissions are checked.
430 * @param $cache
431 * Boolean whether to cache the filtered output in the {cache_filter} table.
432 * The caller may set this to FALSE when the output is already cached
433 * elsewhere to avoid duplicate cache lookups and storage.
434 */
435 function check_markup($text, $format = FILTER_FORMAT_DEFAULT, $langcode = '', $check = TRUE, $cache = TRUE) {
436 // When $check = TRUE, do an access check on $format.
437 if (isset($text) && (!$check || filter_access($format))) {
438 $format = filter_resolve_format($format);
439
440 // Check for a cached version of this piece of text.
441 $cache_id = $format . ':' . $langcode . ':' . md5($text);
442 if ($cache && $cached = cache_get($cache_id, 'cache_filter')) {
443 return $cached->data;
444 }
445
446 // Convert all Windows and Mac newlines to a single newline,
447 // so filters only need to deal with one possibility.
448 $text = str_replace(array("\r\n", "\r"), "\n", $text);
449
450 // Get a complete list of filters, ordered properly.
451 $filters = filter_list_format($format);
452
453 // Give filters the chance to escape HTML-like data such as code or formulas.
454 foreach ($filters as $filter) {
455 $filter_info = module_invoke($filter->module, 'filter_info');
456 if (isset($filter_info[$filter->delta]['prepare callback']) && drupal_function_exists($filter_info[$filter->delta]['prepare callback'])) {
457 $text = call_user_func($filter_info[$filter->delta]['prepare callback'], $text, $format, $langcode, $cache_id);
458 }
459 }
460
461 // Perform filtering.
462 foreach ($filters as $filter) {
463 $filter_info = module_invoke($filter->module, 'filter_info');
464 if (isset($filter_info[$filter->delta]['process callback']) && drupal_function_exists($filter_info[$filter->delta]['process callback'])) {
465 $text = call_user_func($filter_info[$filter->delta]['process callback'], $text, $format, $langcode, $cache_id);
466 }
467 }
468
469 // Store in cache with a minimum expiration time of 1 day.
470 if ($cache && filter_format_allowcache($format)) {
471 cache_set($cache_id, $text, 'cache_filter', REQUEST_TIME + (60 * 60 * 24));
472 }
473 }
474 else {
475 $text = t('n/a');
476 }
477
478 return $text;
479 }
480
481 /**
482 * Generate a selector for choosing a format in a form.
483 *
484 * @ingroup forms
485 * @param $value
486 * The ID of the format that is currently selected.
487 * @param $weight
488 * The weight of the text format.
489 * @param $parents
490 * Required when defining multiple text formats on a single node or having a different parent than 'format'.
491 * @return
492 * HTML for the form element.
493 */
494 function filter_form($value = FILTER_FORMAT_DEFAULT, $weight = NULL, $parents = array('format')) {
495 $value = filter_resolve_format($value);
496 $formats = filter_formats();
497
498 drupal_add_js('misc/form.js');
499 drupal_add_css(drupal_get_path('module', 'filter') . '/filter.css');
500 $element_id = form_clean_id('edit-' . implode('-', $parents));
501
502 $form = array(
503 '#type' => 'fieldset',
504 '#weight' => $weight,
505 '#attributes' => array('class' => 'filter-wrapper'),
506 );
507 $form['format_guidelines'] = array(
508 '#prefix' => '<div id="' . $element_id . '-guidelines" class="filter-guidelines">',
509 '#suffix' => '</div>',
510 '#weight' => 2,
511 );
512 foreach ($formats as $format) {
513 $options[$format->format] = $format->name;
514 $form['format_guidelines'][$format->format] = array(
515 '#markup' => theme('filter_guidelines', $format),
516 );
517 }
518 $form['format'] = array(
519 '#type' => 'select',
520 '#title' => t('Text format'),
521 '#options' => $options,
522 '#default_value' => $value,
523 '#parents' => $parents,
524 '#access' => count($formats) > 1,
525 '#id' => $element_id,
526 '#attributes' => array('class' => 'filter-list'),
527 );
528 $form['format_help'] = array(
529 '#prefix' => '<div id="' . $element_id . '-help" class="filter-help">',
530 '#markup' => theme('filter_tips_more_info'),
531 '#suffix' => '</div>',
532 '#weight' => 1,
533 );
534
535 return $form;
536 }
537
538 /**
539 * Returns TRUE if the user is allowed to access this format.
540 */
541 function filter_access($format) {
542 $format = filter_resolve_format($format);
543 if (user_access('administer filters') || ($format == variable_get('filter_default_format', 1))) {
544 return TRUE;
545 }
546 else {
547 $formats = filter_formats();
548 return isset($formats[$format]);
549 }
550 }
551
552 /**
553 * @} End of "Filtering functions".
554 */
555
556
557 /**
558 * Helper function for fetching filter tips.
559 */
560 function _filter_tips($format, $long = FALSE) {
561 if ($format == -1) {
562 $formats = filter_formats();
563 }
564 else {
565 $formats = array(db_query("SELECT * FROM {filter_format} WHERE format = :format", array(':format' => $format))->fetchObject());
566 }
567
568 $tips = array();
569
570 foreach ($formats as $format) {
571 $filters = filter_list_format($format->format);
572
573 $tips[$format->name] = array();
574 foreach ($filters as $id => $filter) {
575 $filter_info = module_invoke($filter->module, 'filter_info');
576 if (isset($filter_info[$filter->delta]['tips callback']) && drupal_function_exists($filter_info[$filter->delta]['tips callback'])) {
577 $tip = call_user_func($filter_info[$filter->delta]['tips callback'],$format->format, $long);
578 $tips[$format->name][] = array('tip' => $tip, 'id' => $id);
579 }
580 }
581 }
582
583 return $tips;
584 }
585
586 /**
587 * Parses an HTML snippet and returns it as a DOM object.
588 *
589 * This function loads the body part of a partial (X)HTML document
590 * and returns a full DOMDocument object that represents this document.
591 * You can use filter_dom_serialize() to serialize this DOMDocument
592 * back to a XHTML snippet.
593 *
594 * @param $text
595 * The partial (X)HTML snippet to load. Invalid mark-up
596 * will be corrected on import.
597 * @return
598 * A DOMDocument that represents the loaded (X)HTML snippet.
599 */
600 function filter_dom_load($text) {
601 // Ignore warnings during HTML soup loading.
602 $dom_document = @DOMDocument::loadHTML('<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $text . '</body></html>');
603
604 return $dom_document;
605 }
606
607 /**
608 * Converts a DOM object back to an HTML snippet.
609 *
610 * The function serializes the body part of a DOMDocument
611 * back to an XHTML snippet.
612 *
613 * The resulting XHTML snippet will be properly formatted
614 * to be compatible with HTML user agents.
615 *
616 * @param $dom_document
617 * A DOMDocument object to serialize, only the tags below
618 * the first <body> node will be converted.
619 * @return
620 * A valid (X)HTML snippet, as a string.
621 */
622 function filter_dom_serialize($dom_document) {
623 $body_node = $dom_document->getElementsByTagName('body')->item(0);
624 $body_content = '';
625 foreach ($body_node->childNodes as $child_node) {
626 $body_content .= $dom_document->saveXML($child_node);
627 }
628 return preg_replace('|<([^>]*)/>|i', '<$1 />', $body_content);
629 }
630
631 /**
632 * Format a link to the more extensive filter tips.
633 *
634 * @ingroup themeable
635 */
636 function theme_filter_tips_more_info() {
637 return '<p>' . l(t('More information about text formats'), 'filter/tips') . '</p>';
638 }
639
640 /**
641 * Format guidelines for a text format.
642 *
643 * @ingroup themeable
644 */
645 function theme_filter_guidelines($format) {
646 $name = isset($format->name) ? '<label>' . $format->name . ':</label>' : '';
647 return '<div id="filter-guidelines-' . $format->format . '" class="filter-guidelines-item">' . $name . theme('filter_tips', _filter_tips($format->format, FALSE)) . '</div>';
648 }
649
650 /**
651 * @name Standard filters
652 * @{
653 * Filters implemented by the filter.module.
654 */
655
656 function filter_filter_info() {
657 $filters[0] = array(
658 'name' => t('Limit allowed HTML tags'),
659 'description' => 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.'),
660 'process callback' => '_filter_html',
661 'settings callback' => '_filter_html_settings',
662 'tips callback' => '_filter_html_tips'
663 );
664 $filters[1] = array(
665 'name' => t('Convert line breaks'),
666 'description' => t('Converts line breaks into HTML (i.e. &lt;br&gt; and &lt;p&gt;) tags.'),
667 'process callback' => '_filter_autop',
668 'tips callback' => '_filter_autop_tips'
669 );
670 $filters[2] = array(
671 'name' => t('Convert URLs into links'),
672 'description' => t('Turns web and e-mail addresses into clickable links.'),
673 'process callback' => '_filter_url',
674 'settings callback' => '_filter_url_settings',
675 'tips callback' => '_filter_url_tips'
676 );
677 $filters[3] = array(
678 'name' => t('Correct broken HTML'),
679 'description' => t('Corrects faulty and chopped off HTML in postings.'),
680 'process callback' => '_filter_htmlcorrector',
681 );
682 $filters[4] = array(
683 'name' => t('Escape all HTML'),
684 'description' => t('Escapes all HTML tags, so they will be visible instead of being effective.'),
685 'process callback' => '_filter_html_escape',
686 'tips callback' => '_filter_html_escape_tips'
687 );
688 return $filters;
689 }
690
691 /**
692 * Settings for the HTML filter.
693 */
694 function _filter_html_settings($format) {
695 $form['filter_html'] = array(
696 '#type' => 'fieldset',
697 '#title' => t('HTML filter'),
698 '#collapsible' => TRUE,
699 );
700 $form['filter_html']["allowed_html_$format"] = array(
701 '#type' => 'textfield',
702 '#title' => t('Allowed HTML tags'),
703 '#default_value' => variable_get("allowed_html_$format", '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>'),
704 '#size' => 64,
705 '#maxlength' => 1024,
706 '#description' => t('Specify a list of tags which should not be stripped. (Note that JavaScript event attributes are always stripped.)'),
707 );
708 $form['filter_html']["filter_html_help_$format"] = array(
709 '#type' => 'checkbox',
710 '#title' => t('Display HTML help'),
711 '#default_value' => variable_get("filter_html_help_$format", 1),
712 '#description' => t('If enabled, Drupal will display some basic HTML help in the long filter tips.'),
713 );
714 $form['filter_html']["filter_html_nofollow_$format"] = array(
715 '#type' => 'checkbox',
716 '#title' => t('Spam link deterrent'),
717 '#default_value' => variable_get("filter_html_nofollow_$format", FALSE),
718 '#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.'),
719 );
720 return $form;
721 }
722
723 /**
724 * HTML filter. Provides filtering of input into accepted HTML.
725 */
726 function _filter_html($text, $format) {
727 $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);
728 $text = filter_xss($text, $allowed_tags);
729
730 if (variable_get("filter_html_nofollow_$format", FALSE)) {
731 $html_dom = filter_dom_load($text);
732 $links = $html_dom->getElementsByTagName('a');
733 foreach($links as $link) {
734 $link->setAttribute('rel', 'nofollow');
735 }
736 $text = filter_dom_serialize($html_dom);
737 }
738
739 return trim($text);
740 }
741
742 /**
743 * Settings for URL filter.
744 */
745 function _filter_url_settings($format) {
746 $form['filter_urlfilter'] = array(
747 '#type' => 'fieldset',
748 '#title' => t('URL filter'),
749 '#collapsible' => TRUE,
750 );
751 $form['filter_urlfilter']['filter_url_length_' . $format] = array(
752 '#type' => 'textfield',
753 '#title' => t('Maximum link text length'),
754 '#default_value' => variable_get('filter_url_length_' . $format, 72),
755 '#maxlength' => 4,
756 '#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.'),
757 );
758 return $form;
759 }
760
761 /**
762 * URL filter. Automatically converts text web addresses (URLs, e-mail addresses,
763 * ftp links, etc.) into hyperlinks.
764 */
765 function _filter_url($text, $format) {
766 // Pass length to regexp callback
767 _filter_url_trim(NULL, variable_get('filter_url_length_' . $format, 72));
768
769 $text = ' ' . $text . ' ';
770
771 // Match absolute URLs.
772 $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);
773
774 // Match e-mail addresses.
775 $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);
776
777 // Match www domains/addresses.
778 $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);
779 $text = substr($text, 1, -1);
780
781 return $text;
782 }
783
784 /**
785 * Scan input and make sure that all HTML tags are properly closed and nested.
786 */
787 function _filter_htmlcorrector($text) {
788 return filter_dom_serialize(filter_dom_load($text));
789 }
790
791 /**
792 * Make links out of absolute URLs.
793 */
794 function _filter_url_parse_full_links($match) {
795 $match[2] = decode_entities($match[2]);
796 $caption = check_plain(_filter_url_trim($match[2]));
797 $match[2] = check_url($match[2]);
798 return $match[1] . '<a href="' . $match[2] . '" title="' . $match[2] . '">' . $caption . '</a>' . $match[5];
799 }
800
801 /**
802 * Make links out of domain names starting with "www."
803 */
804 function _filter_url_parse_partial_links($match) {
805 $match[2] = decode_entities($match[2]);
806 $caption = check_plain(_filter_url_trim($match[2]));
807 $match[2] = check_plain($match[2]);
808 return $match[1] . '<a href="http://' . $match[2] . '" title="' . $match[2] . '">' . $caption . '</a>' . $match[3];
809 }
810
811 /**
812 * Shortens long URLs to http://www.example.com/long/url...
813 */
814 function _filter_url_trim($text, $length = NULL) {
815 static $_length;
816 if ($length !== NULL) {
817 $_length = $length;
818 }
819
820 // Use +3 for '...' string length.
821 if (strlen($text) > $_length + 3) {
822 $text = substr($text, 0, $_length) . '...';
823 }
824
825 return $text;
826 }
827
828 /**
829 * Convert line breaks into <p> and <br> in an intelligent fashion.
830 * Based on: http://photomatt.net/scripts/autop
831 */
832 function _filter_autop($text) {
833 // All block level tags
834 $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)';
835
836 // Split at <pre>, <script>, <style> and </pre>, </script>, </style> tags.
837 // We don't apply any processing to the contents of these tags to avoid messing
838 // up code. We look for matched pairs and allow basic nesting. For example:
839 // "processed <pre> ignored <script> ignored </script> ignored </pre> processed"
840 $chunks = preg_split('@(</?(?:pre|script|style|object)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
841 // Note: PHP ensures the array consists of alternating delimiters and literals
842 // and begins and ends with a literal (inserting NULL as required).
843 $ignore = FALSE;
844 $ignoretag = '';
845 $output = '';
846 foreach ($chunks as $i => $chunk) {
847 if ($i % 2) {
848 // Opening or closing tag?
849 $open = ($chunk[1] != '/');
850 list($tag) = preg_split('/[ >]/', substr($chunk, 2 - $open), 2);
851 if (!$ignore) {
852 if ($open) {
853 $ignore = TRUE;
854 $ignoretag = $tag;
855 }
856 }
857 // Only allow a matching tag to close it.
858 elseif (!$open && $ignoretag == $tag) {
859 $ignore = FALSE;
860 $ignoretag = '';
861 }
862 }
863 elseif (!$ignore) {
864 $chunk = preg_replace('|\n*$|', '', $chunk) . "\n\n"; // just to make things a little easier, pad the end
865 $chunk = preg_replace('|<br />\s*<br />|', "\n\n", $chunk);
866 $chunk = preg_replace('!(<' . $block . '[^>]*>)!', "\n$1", $chunk); // Space things out a little
867 $chunk = preg_replace('!(</' . $block . '>)!', "$1\n\n", $chunk); // Space things out a little
868 $chunk = preg_replace("/\n\n+/", "\n\n", $chunk); // take care of duplicates
869 $chunk = preg_replace('/^\n|\n\s*\n$/', '', $chunk);
870 $chunk = '<p>' . preg_replace('/\n\s*\n\n?(.)/', "</p>\n<p>$1", $chunk) . "</p>\n"; // make paragraphs, including one at the end
871 $chunk = preg_replace("|<p>(<li.+?)</p>|", "$1", $chunk); // problem with nested lists
872 $chunk = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $chunk);
873 $chunk = str_replace('</blockquote></p>', '</p></blockquote>', $chunk);
874 $chunk = preg_replace('|<p>\s*</p>\n?|', '', $chunk); // under certain strange conditions it could create a P of entirely whitespace
875 $chunk = preg_replace('!<p>\s*(</?' . $block . '[^>]*>)!', "$1", $chunk);
876 $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*</p>!', "$1", $chunk);
877 $chunk = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $chunk); // make line breaks
878 $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*<br />!', "$1", $chunk);
879 $chunk = preg_replace('!<br />(\s*</?(?:p|li|div|th|pre|td|ul|ol)>)!', '$1', $chunk);
880 $chunk = preg_replace('/&([^#])(?![A-Za-z0-9]{1,8};)/', '&amp;$1', $chunk);
881 }
882 $output .= $chunk;
883 }
884 return $output;
885 }
886
887 /**
888 * @} End of "Standard filters".
889 */
890

Legend

Missed
lines code that were not excersized during program execution.
Covered
lines code were excersized during program execution.
Comment/non executable
Comment or non-executable line of code.
Dead
lines of code that according to xdebug could not be executed. This is counted as coverage code because in almost all cases it is code that runnable.