/[drupal]/drupal/modules/filter/filter.module
ViewVC logotype

Contents of /drupal/modules/filter/filter.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.300 - (show annotations) (download) (as text)
Fri Oct 23 22:24:14 2009 UTC (5 weeks ago) by webchick
Branch: MAIN
CVS Tags: DRUPAL-7-0-UNSTABLE-10
Changes since 1.299: +6 -6 lines
File MIME type: text/x-php
#600974 by effulgentsia, JohnAlbin, sun, and Damien Tournoud: Allow theme functions to take one argument without any hacks. NOTE: This is an API change in hook_theme().
1 <?php
2 // $Id: filter.module,v 1.299 2009/10/23 01:06:58 dries Exp $
3
4 /**
5 * @file
6 * Framework for handling filtering of content.
7 */
8
9 /**
10 * Implement hook_help().
11 */
12 function filter_help($path, $arg) {
13 switch ($path) {
14 case 'admin/help#filter':
15 $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>';
16 $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>';
17 $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 and control the order of formats listed in the <em>Text format</em> fieldset.') . '</p>';
18 $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>';
19 return $output;
20 case 'admin/config/content/formats':
21 $output = '<p>' . t('Use the list below to review the text formats available to each user role 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.) All text formats are available to users in roles with the "administer filters" permission, and the special %fallback format is available to all users. You can configure access to other text formats on the <a href="@url">permissions page</a>.', array('%fallback' => filter_fallback_format_title(), '@url' => url('admin/config/people/permissions', array('fragment' => 'module-filter')))) . '</p>';
22 $output .= '<p>' . t('Since text formats, if available, are presented in the same order as the list below, and the default format for each user is the first one on the list for which that user has access, 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>';
23 return $output;
24 case 'admin/config/content/formats/%':
25 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/config/content/formats/' . $arg[4] . '/order'))) . '</p>';
26 case 'admin/config/content/formats/%/configure':
27 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/config/content/formats/' . $arg[4]))) . '</p>';
28 case 'admin/config/content/formats/%/order':
29 $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>';
30 $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>';
31 return $output;
32 }
33 }
34
35 /**
36 * Implement hook_theme().
37 */
38 function filter_theme() {
39 return array(
40 'filter_admin_overview' => array(
41 'render element' => 'form',
42 'file' => 'filter.admin.inc',
43 ),
44 'filter_admin_order' => array(
45 'render element' => 'form',
46 'file' => 'filter.admin.inc',
47 ),
48 'filter_tips' => array(
49 'variables' => array('tips' => NULL, 'long' => FALSE),
50 'file' => 'filter.pages.inc',
51 ),
52 'filter_tips_more_info' => array(
53 'variables' => array(),
54 ),
55 'filter_guidelines' => array(
56 'variables' => array('format' => NULL),
57 ),
58 );
59 }
60
61 /**
62 * Implement hook_menu().
63 */
64 function filter_menu() {
65 $items['filter/tips'] = array(
66 'title' => 'Compose tips',
67 'page callback' => 'filter_tips_long',
68 'access callback' => TRUE,
69 'type' => MENU_SUGGESTED_ITEM,
70 'file' => 'filter.pages.inc',
71 );
72 $items['admin/config/content/formats'] = array(
73 'title' => 'Text formats',
74 'description' => 'Configure how content input by users is filtered, including allowed HTML tags. Also allows enabling of module-provided filters.',
75 'page callback' => 'drupal_get_form',
76 'page arguments' => array('filter_admin_overview'),
77 'access arguments' => array('administer filters'),
78 'file' => 'filter.admin.inc',
79 );
80 $items['admin/config/content/formats/list'] = array(
81 'title' => 'List',
82 'type' => MENU_DEFAULT_LOCAL_TASK,
83 );
84 $items['admin/config/content/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_ACTION,
89 'weight' => 1,
90 'file' => 'filter.admin.inc',
91 );
92 $items['admin/config/content/formats/%filter_format'] = array(
93 'type' => MENU_CALLBACK,
94 'title callback' => 'filter_admin_format_title',
95 'title arguments' => array(4),
96 'page callback' => 'filter_admin_format_page',
97 'page arguments' => array(4),
98 'access arguments' => array('administer filters'),
99 'file' => 'filter.admin.inc',
100 );
101 $items['admin/config/content/formats/%filter_format/edit'] = array(
102 'title' => 'Edit',
103 'type' => MENU_DEFAULT_LOCAL_TASK,
104 'weight' => 0,
105 );
106 $items['admin/config/content/formats/%filter_format/configure'] = array(
107 'title' => 'Configure',
108 'page callback' => 'filter_admin_configure_page',
109 'page arguments' => array(4),
110 'access arguments' => array('administer filters'),
111 'type' => MENU_LOCAL_TASK,
112 'weight' => 1,
113 'file' => 'filter.admin.inc',
114 );
115 $items['admin/config/content/formats/%filter_format/order'] = array(
116 'title' => 'Rearrange',
117 'page callback' => 'filter_admin_order_page',
118 'page arguments' => array(4),
119 'access arguments' => array('administer filters'),
120 'type' => MENU_LOCAL_TASK,
121 'weight' => 2,
122 'file' => 'filter.admin.inc',
123 );
124 $items['admin/config/content/formats/%filter_format/delete'] = array(
125 'title' => 'Delete text format',
126 'page callback' => 'drupal_get_form',
127 'page arguments' => array('filter_admin_delete', 4),
128 'access callback' => '_filter_delete_format_access',
129 'access arguments' => array(4),
130 'type' => MENU_CALLBACK,
131 'file' => 'filter.admin.inc',
132 );
133 return $items;
134 }
135
136 /**
137 * Access callback for deleting text formats.
138 *
139 * @param $format
140 * A text format object.
141 * @return
142 * TRUE if the text format can be deleted by the current user, FALSE
143 * otherwise.
144 */
145 function _filter_delete_format_access($format) {
146 // The fallback format can never be deleted.
147 return user_access('administer filters') && ($format->format != filter_fallback_format());
148 }
149
150 /**
151 * Load a text format object from the database.
152 *
153 * @param $format_id
154 * The format ID.
155 *
156 * @return
157 * A fully-populated text format object.
158 */
159 function filter_format_load($format_id) {
160 $formats = filter_formats();
161 return isset($formats[$format_id]) ? $formats[$format_id] : FALSE;
162 }
163
164 /**
165 * Save a text format object to the database.
166 *
167 * @param $format
168 * A format object using the properties:
169 * - 'name': The title of the text format.
170 * - 'format': (optional) The internal ID of the text format. If omitted, a
171 * new text format is created.
172 * - 'roles': (optional) An associative array containing the roles allowed to
173 * access/use the text format.
174 * - 'filters': (optional) An associative, multi-dimensional array of filters
175 * assigned to the text format, using the properties:
176 * - 'weight': The weight of the filter in the text format.
177 * - 'status': A boolean indicating whether the filter is enabled in the
178 * text format.
179 * - 'module': The name of the module implementing the filter.
180 * - 'settings': (optional) An array of configured settings for the filter.
181 * See hook_filter_info() for details.
182 */
183 function filter_format_save(&$format) {
184 $format->name = trim($format->name);
185
186 // Add a new text format.
187 if (empty($format->format)) {
188 $return = drupal_write_record('filter_format', $format);
189 }
190 else {
191 $return = drupal_write_record('filter_format', $format, 'format');
192 }
193
194 // Get the filters currently active in the format, to add new filters
195 // to the bottom.
196 $current = filter_list_format($format->format, TRUE);
197 $filter_info = filter_get_filters();
198 if (!isset($format->filters)) {
199 $format->filters = array();
200 }
201 foreach ($format->filters as $name => $filter) {
202 $fields = array();
203 // Add new filters to the bottom.
204 $fields['weight'] = isset($current[$name]->weight) ? $current[$name]->weight : 10;
205 $fields['status'] = $filter['status'];
206 $fields['module'] = $filter_info[$name]['module'];
207 $format->filters[$name]['module'] = $filter_info[$name]['module'];
208 // Only update settings if there are any.
209 if (!empty($filter['settings'])) {
210 $fields['settings'] = serialize($filter['settings']);
211 }
212 db_merge('filter')
213 ->key(array(
214 'format' => $format->format,
215 'name' => $name,
216 ))
217 ->fields($fields)
218 ->execute();
219 }
220
221 if ($return == SAVED_NEW) {
222 module_invoke_all('filter_format_insert', $format);
223 }
224 else {
225 module_invoke_all('filter_format_update', $format);
226 // Explicitly indicate that the format was updated. We need to do this
227 // since if the filters were updated but the format object itself was not,
228 // the call to drupal_write_record() above would not return an indication
229 // that anything had changed.
230 $return = SAVED_UPDATED;
231
232 // Clear the filter cache whenever a text format is updated.
233 cache_clear_all($format->format . ':', 'cache_filter', TRUE);
234 }
235
236 filter_formats_reset();
237
238 return $return;
239 }
240
241 /**
242 * Delete a text format.
243 *
244 * @param $format
245 * The text format object to be deleted.
246 */
247 function filter_format_delete($format) {
248 db_delete('filter_format')
249 ->condition('format', $format->format)
250 ->execute();
251 db_delete('filter')
252 ->condition('format', $format->format)
253 ->execute();
254
255 // Allow modules to react on text format deletion.
256 $fallback = filter_format_load(filter_fallback_format());
257 module_invoke_all('filter_format_delete', $format, $fallback);
258
259 filter_formats_reset();
260 cache_clear_all($format->format . ':', 'cache_filter', TRUE);
261 }
262
263 /**
264 * Display a text format form title.
265 */
266 function filter_admin_format_title($format) {
267 return $format->name;
268 }
269
270 /**
271 * Implement hook_permission().
272 */
273 function filter_permission() {
274 $perms['administer filters'] = array(
275 'title' => t('Administer filters'),
276 'description' => t('Manage text formats and filters, and use any of them, without restriction, when entering or editing content. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
277 );
278
279 // Generate permissions for each text format. Warn the administrator that any
280 // of them are potentially unsafe.
281 foreach (filter_formats() as $format) {
282 $permission = filter_permission_name($format);
283 if (!empty($permission)) {
284 // Only link to the text format configuration page if the user who is
285 // viewing this will have access to that page.
286 $format_name_replacement = user_access('administer filters') ? l($format->name, 'admin/config/content/formats/' . $format->format) : theme('placeholder', array('text' => $format->name));
287 $perms[$permission] = array(
288 'title' => t("Use the %text_format text format", array('%text_format' => $format->name)),
289 'description' => t('Use !text_format in forms when entering or editing content. %warning', array('!text_format' => $format_name_replacement, '%warning' => t('Warning: This permission may have security implications depending on how the text format is configured.'))),
290 );
291 }
292 }
293 return $perms;
294 }
295
296 /**
297 * Returns the machine-readable permission name for a provided text format.
298 *
299 * @param $format
300 * An object representing a text format.
301 * @return
302 * The machine-readable permission name, or FALSE if the provided text format
303 * is malformed or is the fallback format (which is available to all users).
304 */
305 function filter_permission_name($format) {
306 if (isset($format->format) && $format->format != filter_fallback_format()) {
307 return 'use text format ' . $format->format;
308 }
309 return FALSE;
310 }
311
312 /**
313 * Implement hook_cron().
314 *
315 * Expire outdated filter cache entries
316 */
317 function filter_cron() {
318 cache_clear_all(NULL, 'cache_filter');
319 }
320
321 /**
322 * Retrieve a list of text formats, ordered by weight.
323 *
324 * @param $account
325 * (optional) If provided, only those formats that are allowed for this user
326 * account will be returned. All formats will be returned otherwise.
327 * @return
328 * An array of text format objects, keyed by the format ID and ordered by
329 * weight.
330 *
331 * @see filter_formats_reset()
332 */
333 function filter_formats($account = NULL) {
334 $formats = &drupal_static(__FUNCTION__, array());
335
336 // Statically cache all existing formats upfront.
337 if (!isset($formats['all'])) {
338 $formats['all'] = db_select('filter_format', 'ff')
339 ->addTag('translatable')
340 ->fields('ff')
341 ->orderBy('weight')
342 ->execute()
343 ->fetchAllAssoc('format');
344 }
345
346 // Build a list of user-specific formats.
347 if (isset($account) && !isset($formats['user'][$account->uid])) {
348 $formats['user'][$account->uid] = array();
349 foreach ($formats['all'] as $format) {
350 if (filter_access($format, $account)) {
351 $formats['user'][$account->uid][$format->format] = $format;
352 }
353 }
354 }
355
356 return isset($account) ? $formats['user'][$account->uid] : $formats['all'];
357 }
358
359 /**
360 * Resets the static cache of all text formats.
361 *
362 * @see filter_formats()
363 */
364 function filter_formats_reset() {
365 drupal_static_reset('filter_list_format');
366 drupal_static_reset('filter_formats');
367 }
368
369 /**
370 * Retrieves a list of roles that are allowed to use a given text format.
371 *
372 * @param $format
373 * An object representing the text format.
374 * @return
375 * An array of role names, keyed by role ID.
376 */
377 function filter_get_roles_by_format($format) {
378 // Handle the fallback format upfront (all roles have access to this format).
379 if ($format->format == filter_fallback_format()) {
380 return user_roles();
381 }
382 // Do not list any roles if the permission does not exist.
383 $permission = filter_permission_name($format);
384 return !empty($permission) ? user_roles(FALSE, $permission) : array();
385 }
386
387 /**
388 * Retrieves a list of text formats that are allowed for a given role.
389 *
390 * @param $rid
391 * The user role ID to retrieve text formats for.
392 * @return
393 * An array of text format objects that are allowed for the role, keyed by
394 * the text format ID and ordered by weight.
395 */
396 function filter_get_formats_by_role($rid) {
397 $formats = array();
398 foreach (filter_formats() as $format) {
399 $roles = filter_get_roles_by_format($format);
400 if (isset($roles[$rid])) {
401 $formats[$format->format] = $format;
402 }
403 }
404 return $formats;
405 }
406
407 /**
408 * Returns the ID of the default text format for a particular user.
409 *
410 * The default text format is the first available format that the user is
411 * allowed to access, when the formats are ordered by weight. It should
412 * generally be used as a default choice when presenting the user with a list
413 * of possible text formats (for example, in a node creation form).
414 *
415 * Conversely, when existing content that does not have an assigned text format
416 * needs to be filtered for display, the default text format is the wrong
417 * choice, because it is not guaranteed to be consistent from user to user, and
418 * some trusted users may have an unsafe text format set by default, which
419 * should not be used on text of unknown origin. Instead, the fallback format
420 * returned by filter_fallback_format() should be used, since that is intended
421 * to be a safe, consistent format that is always available to all users.
422 *
423 * @param $account
424 * (optional) The user account to check. Defaults to the currently logged-in
425 * user.
426 * @return
427 * The ID of the user's default text format.
428 *
429 * @see filter_fallback_format()
430 */
431 function filter_default_format($account = NULL) {
432 global $user;
433 if (!isset($account)) {
434 $account = $user;
435 }
436 // Get a list of formats for this user, ordered by weight. The first one
437 // available is the user's default format.
438 $format = array_shift(filter_formats($account));
439 return $format->format;
440 }
441
442 /**
443 * Returns the ID of the fallback text format that all users have access to.
444 */
445 function filter_fallback_format() {
446 // This variable is automatically set in the database for all installations
447 // of Drupal. In the event that it gets deleted somehow, there is no safe
448 // default to return, since we do not want to risk making an existing (and
449 // potentially unsafe) text format on the site automatically available to all
450 // users. Returning NULL at least guarantees that this cannot happen.
451 return variable_get('filter_fallback_format');
452 }
453
454 /**
455 * Returns the title of the fallback text format.
456 */
457 function filter_fallback_format_title() {
458 $fallback_format = filter_format_load(filter_fallback_format());
459 return filter_admin_format_title($fallback_format);
460 }
461
462 /**
463 * Return a list of all filters provided by modules.
464 */
465 function filter_get_filters() {
466 $filters = &drupal_static(__FUNCTION__, array());
467
468 if (empty($filters)) {
469 foreach (module_implements('filter_info') as $module) {
470 $info = module_invoke($module, 'filter_info');
471 if (isset($info) && is_array($info)) {
472 // Assign the name of the module implementing the filters.
473 foreach (array_keys($info) as $name) {
474 $info[$name]['module'] = $module;
475 }
476 $filters = array_merge($filters, $info);
477 }
478 }
479 // Allow modules to alter filter definitions.
480 drupal_alter('filter_info', $filters);
481
482 uasort($filters, '_filter_list_cmp');
483 }
484
485 return $filters;
486 }
487
488 /**
489 * Helper function for sorting the filter list by filter name.
490 */
491 function _filter_list_cmp($a, $b) {
492 return strcmp($a['title'], $b['title']);
493 }
494
495 /**
496 * Check if text in a certain text format is allowed to be cached.
497 */
498 function filter_format_allowcache($format_id) {
499 static $cache = array();
500 if (!isset($cache[$format_id])) {
501 $cache[$format_id] = db_query('SELECT cache FROM {filter_format} WHERE format = :format', array(':format' => $format_id))->fetchField();
502 }
503 return $cache[$format_id];
504 }
505
506 /**
507 * Retrieve a list of filters for a given text format.
508 *
509 * @param $format_id
510 * The format ID.
511 *
512 * @return
513 * An array of filter objects assosiated to the given format.
514 */
515 function filter_list_format($format_id) {
516 $filters = &drupal_static(__FUNCTION__, array());
517 $filter_info = filter_get_filters();
518
519 if (!isset($filters[$format_id])) {
520 $format_filters = array();
521 $query = db_select('filter', 'filter')
522 ->fields('filter')
523 ->condition('format', $format_id)
524 ->orderBy('weight')
525 ->orderBy('module')
526 ->orderBy('name');
527 $result = $query->execute()->fetchAllAssoc('name');
528 foreach ($result as $name => $filter) {
529 if (isset($filter_info[$name])) {
530 $filter->title = $filter_info[$name]['title'];
531 // Unpack stored filter settings.
532 $filter->settings = (isset($filter->settings) ? unserialize($filter->settings) : array());
533 // Apply default filter settings.
534 if (isset($filter_info[$name]['default settings'])) {
535 $filter->settings = array_merge($filter_info[$name]['default settings'], $filter->settings);
536 }
537 $format_filters[$name] = $filter;
538 }
539 }
540 $filters[$format_id] = $format_filters;
541 }
542
543 return isset($filters[$format_id]) ? $filters[$format_id] : array();
544 }
545
546 /**
547 * Run all the enabled filters on a piece of text.
548 *
549 * Note: Because filters can inject JavaScript or execute PHP code, security is
550 * vital here. When a user supplies a text format, you should validate it using
551 * filter_access() before accepting/using it. This is normally done in the
552 * validation stage of the Form API. You should for example never make a preview
553 * of content in a disallowed format.
554 *
555 * @param $text
556 * The text to be filtered.
557 * @param $format_id
558 * The format id of the text to be filtered. If no format is assigned, the
559 * fallback format will be used.
560 * @param $langcode
561 * Optional: the language code of the text to be filtered, e.g. 'en' for
562 * English. This allows filters to be language aware so language specific
563 * text replacement can be implemented.
564 * @param $cache
565 * Boolean whether to cache the filtered output in the {cache_filter} table.
566 * The caller may set this to FALSE when the output is already cached
567 * elsewhere to avoid duplicate cache lookups and storage.
568 */
569 function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) {
570 if (empty($format_id)) {
571 $format_id = filter_fallback_format();
572 }
573 $format = filter_format_load($format_id);
574
575 // Check for a cached version of this piece of text.
576 $cache = $cache && filter_format_allowcache($format->format);
577 $cache_id = '';
578 if ($cache) {
579 $cache_id = $format->format . ':' . $langcode . ':' . md5($text);
580 if ($cached = cache_get($cache_id, 'cache_filter')) {
581 return $cached->data;
582 }
583 }
584
585 // Convert all Windows and Mac newlines to a single newline, so filters only
586 // need to deal with one possibility.
587 $text = str_replace(array("\r\n", "\r"), "\n", $text);
588
589 // Get a complete list of filters, ordered properly.
590 $filters = filter_list_format($format->format);
591 $filter_info = filter_get_filters();
592
593 // Give filters the chance to escape HTML-like data such as code or formulas.
594 foreach ($filters as $name => $filter) {
595 if ($filter->status && isset($filter_info[$name]['prepare callback']) && function_exists($filter_info[$name]['prepare callback'])) {
596 $function = $filter_info[$name]['prepare callback'];
597 $text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
598 }
599 }
600
601 // Perform filtering.
602 foreach ($filters as $name => $filter) {
603 if ($filter->status && isset($filter_info[$name]['process callback']) && function_exists($filter_info[$name]['process callback'])) {
604 $function = $filter_info[$name]['process callback'];
605 $text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
606 }
607 }
608
609 // Store in cache with a minimum expiration time of 1 day.
610 if ($cache) {
611 cache_set($cache_id, $text, 'cache_filter', REQUEST_TIME + (60 * 60 * 24));
612 }
613
614 return $text;
615 }
616
617 /**
618 * Generates a selector for choosing a format in a form.
619 *
620 * @param $selected_format
621 * The ID of the format that is currently selected; uses the default format
622 * for the current user if not provided.
623 * @param $weight
624 * The weight of the form element within the form.
625 * @param $parents
626 * The parents array of the element. Required when defining multiple text
627 * formats on a single form or having a different parent than 'format'.
628 *
629 * @return
630 * Form API array for the form element.
631 *
632 * @ingroup forms
633 */
634 function filter_form($selected_format = NULL, $weight = NULL, $parents = array('format')) {
635 global $user;
636
637 // Use the default format for this user if none was selected.
638 if (empty($selected_format)) {
639 $selected_format = filter_default_format($user);
640 }
641
642 // Get a list of formats that the current user has access to.
643 $formats = filter_formats($user);
644
645 drupal_add_js('misc/form.js');
646 drupal_add_css(drupal_get_path('module', 'filter') . '/filter.css');
647 $element_id = drupal_html_id('edit-' . implode('-', $parents));
648
649 $form = array(
650 '#type' => 'fieldset',
651 '#weight' => $weight,
652 '#attributes' => array('class' => array('filter-wrapper')),
653 );
654 $form['format_guidelines'] = array(
655 '#prefix' => '<div id="' . $element_id . '-guidelines" class="filter-guidelines">',
656 '#suffix' => '</div>',
657 '#weight' => 2,
658 );
659 foreach ($formats as $format) {
660 $options[$format->format] = $format->name;
661 $form['format_guidelines'][$format->format] = array(
662 '#markup' => theme('filter_guidelines', array('format' => $format)),
663 );
664 }
665 $form['format'] = array(
666 '#type' => 'select',
667 '#title' => t('Text format'),
668 '#options' => $options,
669 '#default_value' => $selected_format,
670 '#parents' => $parents,
671 '#access' => count($formats) > 1,
672 '#id' => $element_id,
673 '#attributes' => array('class' => array('filter-list')),
674 );
675 $form['format_help'] = array(
676 '#prefix' => '<div id="' . $element_id . '-help" class="filter-help">',
677 '#markup' => theme('filter_tips_more_info'),
678 '#suffix' => '</div>',
679 '#weight' => 1,
680 );
681
682 return $form;
683 }
684
685 /**
686 * Checks if a user has access to a particular text format.
687 *
688 * @param $format
689 * An object representing the text format.
690 * @param $account
691 * (optional) The user account to check access for; if omitted, the currently
692 * logged-in user is used.
693 *
694 * @return
695 * Boolean TRUE if the user is allowed to access the given format.
696 */
697 function filter_access($format, $account = NULL) {
698 global $user;
699 if (!isset($account)) {
700 $account = $user;
701 }
702 // Handle special cases up front. All users have access to the fallback
703 // format, and administrators have access to all formats.
704 if (user_access('administer filters', $account) || $format->format == filter_fallback_format()) {
705 return TRUE;
706 }
707 // Check the permission if one exists; otherwise, we have a non-existent
708 // format so we return FALSE.
709 $permission = filter_permission_name($format);
710 return !empty($permission) && user_access($permission, $account);
711 }
712
713 /**
714 * Helper function for fetching filter tips.
715 */
716 function _filter_tips($format_id, $long = FALSE) {
717 global $user;
718
719 $formats = filter_formats($user);
720 $filter_info = filter_get_filters();
721
722 $tips = array();
723
724 // If only listing one format, extract it from the $formats array.
725 if ($format_id != -1) {
726 $formats = array($formats[$format_id]);
727 }
728
729 foreach ($formats as $format) {
730 $filters = filter_list_format($format->format);
731 $tips[$format->name] = array();
732 foreach ($filters as $name => $filter) {
733 if (isset($filter_info[$name]['tips callback']) && function_exists($filter_info[$name]['tips callback'])) {
734 $tip = $filter_info[$name]['tips callback']($filter, $format, $long);
735 $tips[$format->name][$name] = array('tip' => $tip, 'id' => $name);
736 }
737 }
738 }
739
740 return $tips;
741 }
742
743 /**
744 * Parses an HTML snippet and returns it as a DOM object.
745 *
746 * This function loads the body part of a partial (X)HTML document
747 * and returns a full DOMDocument object that represents this document.
748 * You can use filter_dom_serialize() to serialize this DOMDocument
749 * back to a XHTML snippet.
750 *
751 * @param $text
752 * The partial (X)HTML snippet to load. Invalid mark-up
753 * will be corrected on import.
754 * @return
755 * A DOMDocument that represents the loaded (X)HTML snippet.
756 */
757 function filter_dom_load($text) {
758 // Ignore warnings during HTML soup loading.
759 $dom_document = @DOMDocument::loadHTML('<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $text . '</body></html>');
760
761 return $dom_document;
762 }
763
764 /**
765 * Converts a DOM object back to an HTML snippet.
766 *
767 * The function serializes the body part of a DOMDocument
768 * back to an XHTML snippet.
769 *
770 * The resulting XHTML snippet will be properly formatted
771 * to be compatible with HTML user agents.
772 *
773 * @param $dom_document
774 * A DOMDocument object to serialize, only the tags below
775 * the first <body> node will be converted.
776 * @return
777 * A valid (X)HTML snippet, as a string.
778 */
779 function filter_dom_serialize($dom_document) {
780 $body_node = $dom_document->getElementsByTagName('body')->item(0);
781 $body_content = '';
782 foreach ($body_node->childNodes as $child_node) {
783 $body_content .= $dom_document->saveXML($child_node);
784 }
785 return preg_replace('|<([^>]*)/>|i', '<$1 />', $body_content);
786 }
787
788 /**
789 * Format a link to the more extensive filter tips.
790 *
791 * @ingroup themeable
792 */
793 function theme_filter_tips_more_info() {
794 return '<p>' . l(t('More information about text formats'), 'filter/tips') . '</p>';
795 }
796
797 /**
798 * Format guidelines for a text format.
799 *
800 * @param $variables
801 * An associative array containing:
802 * - format: An object representing a text format.
803 *
804 * @ingroup themeable
805 */
806 function theme_filter_guidelines($variables) {
807 $format = $variables['format'];
808
809 $name = isset($format->name) ? '<label>' . $format->name . ':</label>' : '';
810 return '<div id="filter-guidelines-' . $format->format . '" class="filter-guidelines-item">' . $name . theme('filter_tips', array('tips' => _filter_tips($format->format, FALSE))) . '</div>';
811 }
812
813 /**
814 * @name Standard filters
815 * @{
816 * Filters implemented by the filter.module.
817 */
818
819 /**
820 * Implement hook_filter_info().
821 */
822 function filter_filter_info() {
823 $filters['filter_html'] = array(
824 'title' => t('Limit allowed HTML tags'),
825 '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.'),
826 'process callback' => '_filter_html',
827 'settings callback' => '_filter_html_settings',
828 'default settings' => array(
829 'allowed_html' => '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>',
830 'filter_html_help' => 1,
831 'filter_html_nofollow' => 0,
832 ),
833 'tips callback' => '_filter_html_tips',
834 );
835 $filters['filter_autop'] = array(
836 'title' => t('Convert line breaks'),
837 'description' => t('Converts line breaks into HTML (i.e. &lt;br&gt; and &lt;p&gt;) tags.'),
838 'process callback' => '_filter_autop',
839 'tips callback' => '_filter_autop_tips',
840 );
841 $filters['filter_url'] = array(
842 'title' => t('Convert URLs into links'),
843 'description' => t('Turns web and e-mail addresses into clickable links.'),
844 'process callback' => '_filter_url',
845 'settings callback' => '_filter_url_settings',
846 'default settings' => array(
847 'filter_url_length' => 72,
848 ),
849 'tips callback' => '_filter_url_tips',
850 );
851 $filters['filter_htmlcorrector'] = array(
852 'title' => t('Correct broken HTML'),
853 'description' => t('Corrects faulty and chopped off HTML in postings.'),
854 'process callback' => '_filter_htmlcorrector',
855 );
856 $filters['filter_html_escape'] = array(
857 'title' => t('Escape all HTML'),
858 'description' => t('Escapes all HTML tags, so they will be visible instead of being effective.'),
859 'process callback' => '_filter_html_escape',
860 'tips callback' => '_filter_html_escape_tips',
861 );
862 return $filters;
863 }
864
865 /**
866 * Settings callback for the HTML filter.
867 */
868 function _filter_html_settings($form, &$form_state, $filter, $defaults) {
869 $form['allowed_html'] = array(
870 '#type' => 'textfield',
871 '#title' => t('Allowed HTML tags'),
872 '#default_value' => isset($filter->settings['allowed_html']) ? $filter->settings['allowed_html'] : $defaults['allowed_html'],
873 '#size' => 64,
874 '#maxlength' => 1024,
875 '#description' => t('Specify a list of tags which should not be stripped. (Note that JavaScript event attributes are always stripped.)'),
876 );
877 $form['filter_html_help'] = array(
878 '#type' => 'checkbox',
879 '#title' => t('Display HTML help'),
880 '#default_value' => isset($filter->settings['filter_html_help']) ? $filter->settings['filter_html_help'] : $defaults['filter_html_help'],
881 '#description' => t('If enabled, Drupal will display some basic HTML help in the long filter tips.'),
882 );
883 $form['filter_html_nofollow'] = array(
884 '#type' => 'checkbox',
885 '#title' => t('Spam link deterrent'),
886 '#default_value' => isset($filter->settings['filter_html_nofollow']) ? $filter->settings['filter_html_nofollow'] : $defaults['filter_html_nofollow'],
887 '#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.'),
888 );
889 return $form;
890 }
891
892 /**
893 * HTML filter. Provides filtering of input into accepted HTML.
894 */
895 function _filter_html($text, $filter) {
896 $allowed_tags = preg_split('/\s+|<|>/', $filter->settings['allowed_html'], -1, PREG_SPLIT_NO_EMPTY);
897 $text = filter_xss($text, $allowed_tags);
898
899 if ($filter->settings['filter_html_nofollow']) {
900 $html_dom = filter_dom_load($text);
901 $links = $html_dom->getElementsByTagName('a');
902 foreach($links as $link) {
903 $link->setAttribute('rel', 'nofollow');
904 }
905 $text = filter_dom_serialize($html_dom);
906 }
907
908 return trim($text);
909 }
910
911 /**
912 * Filter tips callback for HTML filter.
913 */
914 function _filter_html_tips($filter, $format, $long = FALSE) {
915 global $base_url;
916
917 if (!($allowed_html = $filter->settings['allowed_html'])) {
918 return;
919 }
920 $output = t('Allowed HTML tags: @tags', array('@tags' => $allowed_html));
921 if (!$long) {
922 return $output;
923 }
924
925 $output = '<p>' . $output . '</p>';
926 if (!$filter->settings['filter_html_help']) {
927 return $output;
928 }
929
930 $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>';
931 $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>';
932 $tips = array(
933 'a' => array(t('Anchors are used to make links to other pages.'), '<a href="' . $base_url . '">' . variable_get('site_name', 'Drupal') . '</a>'),
934 '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')),
935 '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>'),
936 'strong' => array(t('Strong'), '<strong>' . t('Strong') . '</strong>'),
937 'em' => array(t('Emphasized'), '<em>' . t('Emphasized') . '</em>'),
938 'cite' => array(t('Cited'), '<cite>' . t('Cited') . '</cite>'),
939 'code' => array(t('Coded text used to show programming source code'), '<code>' . t('Coded') . '</code>'),
940 'b' => array(t('Bolded'), '<b>' . t('Bolded') . '</b>'),
941 'u' => array(t('Underlined'), '<u>' . t('Underlined') . '</u>'),
942 'i' => array(t('Italicized'), '<i>' . t('Italicized') . '</i>'),
943 'sup' => array(t('Superscripted'), t('<sup>Super</sup>scripted')),
944 'sub' => array(t('Subscripted'), t('<sub>Sub</sub>scripted')),
945 'pre' => array(t('Preformatted'), '<pre>' . t('Preformatted') . '</pre>'),
946 'abbr' => array(t('Abbreviation'), t('<abbr title="Abbreviation">Abbrev.</abbr>')),
947 'acronym' => array(t('Acronym'), t('<acronym title="Three-Letter Acronym">TLA</acronym>')),
948 'blockquote' => array(t('Block quoted'), '<blockquote>' . t('Block quoted') . '</blockquote>'),
949 'q' => array(t('Quoted inline'), '<q>' . t('Quoted inline') . '</q>'),
950 // Assumes and describes tr, td, th.
951 'table' => array(t('Table'), '<table> <tr><th>' . t('Table header') . '</th></tr> <tr><td>' . t('Table cell') . '</td></tr> </table>'),
952 'tr' => NULL, 'td' => NULL, 'th' => NULL,
953 'del' => array(t('Deleted'), '<del>' . t('Deleted') . '</del>'),
954 'ins' => array(t('Inserted'), '<ins>' . t('Inserted') . '</ins>'),
955 // Assumes and describes li.
956 '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>'),
957 '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>'),
958 'li' => NULL,
959 // Assumes and describes dt and dd.
960 '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>'),
961 'dt' => NULL, 'dd' => NULL,
962 'h1' => array(t('Heading'), '<h1>' . t('Title') . '</h1>'),
963 'h2' => array(t('Heading'), '<h2>' . t('Subtitle') . '</h2>'),
964 'h3' => array(t('Heading'), '<h3>' . t('Subtitle three') . '</h3>'),
965 'h4' => array(t('Heading'), '<h4>' . t('Subtitle four') . '</h4>'),
966 'h5' => array(t('Heading'), '<h5>' . t('Subtitle five') . '</h5>'),
967 'h6' => array(t('Heading'), '<h6>' . t('Subtitle six') . '</h6>')
968 );
969 $header = array(t('Tag Description'), t('You Type'), t('You Get'));
970 preg_match_all('/<([a-z0-9]+)[^a-z0-9]/i', $allowed_html, $out);
971 foreach ($out[1] as $tag) {
972 if (array_key_exists($tag, $tips)) {
973 if ($tips[$tag]) {
974 $rows[] = array(
975 array('data' => $tips[$tag][0], 'class' => array('description')),
976 array('data' => '<code>' . check_plain($tips[$tag][1]) . '</code>', 'class' => array('type')),
977 array('data' => $tips[$tag][1], 'class' => array('get'))
978 );
979 }
980 }
981 else {
982 $rows[] = array(
983 array('data' => t('No help provided for tag %tag.', array('%tag' => $tag)), 'class' => array('description'), 'colspan' => 3),
984 );
985 }
986 }
987 $output .= theme('table', array('header' => $header, 'rows' => $rows));
988
989 $output .= '<p>' . t('Most unusual characters can be directly entered without any problems.') . '</p>';
990 $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>';
991
992 $entities = array(
993 array(t('Ampersand'), '&amp;'),
994 array(t('Greater than'), '&gt;'),
995 array(t('Less than'), '&lt;'),
996 array(t('Quotation mark'), '&quot;'),
997 );
998 $header = array(t('Character Description'), t('You Type'), t('You Get'));
999 unset($rows);
1000 foreach ($entities as $entity) {
1001 $rows[] = array(
1002 array('data' => $entity[0], 'class' => array('description')),
1003 array('data' => '<code>' . check_plain($entity[1]) . '</code>', 'class' => array('type')),
1004 array('data' => $entity[1], 'class' => array('get'))
1005 );
1006 }
1007 $output .= theme('table', array('header' => $header, 'rows' => $rows));
1008 return $output;
1009 }
1010
1011 /**
1012 * Settings callback for URL filter.
1013 */
1014 function _filter_url_settings($form, &$form_state, $filter, $defaults) {
1015 $form['filter_url_length'] = array(
1016 '#type' => 'textfield',
1017 '#title' => t('Maximum link text length'),
1018 '#default_value' => isset($filter->settings['filter_url_length']) ? $filter->settings['filter_url_length'] : $defaults['filter_url_length'],
1019 '#maxlength' => 4,
1020 '#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.'),
1021 );
1022 return $form;
1023 }
1024
1025 /**
1026 * URL filter. Automatically converts text web addresses (URLs, e-mail addresses,
1027 * ftp links, etc.) into hyperlinks.
1028 */
1029 function _filter_url($text, $filter) {
1030 // Pass length to regexp callback
1031 _filter_url_trim(NULL, $filter->settings['filter_url_length']);
1032
1033 $text = ' ' . $text . ' ';
1034
1035 // Match absolute URLs.
1036 $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);
1037
1038 // Match e-mail addresses.
1039 $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);
1040
1041 // Match www domains/addresses.
1042 $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);
1043 $text = substr($text, 1, -1);
1044
1045 return $text;
1046 }
1047
1048 /**
1049 * Make links out of absolute URLs.
1050 */
1051 function _filter_url_parse_full_links($match) {
1052 $match[2] = decode_entities($match[2]);
1053 $caption = check_plain(_filter_url_trim($match[2]));
1054 $match[2] = check_url($match[2]);
1055 return $match[1] . '<a href="' . $match[2] . '" title="' . $match[2] . '">' . $caption . '</a>' . $match[5];
1056 }
1057
1058 /**
1059 * Make links out of domain names starting with "www."
1060 */
1061 function _filter_url_parse_partial_links($match) {
1062 $match[2] = decode_entities($match[2]);
1063 $caption = check_plain(_filter_url_trim($match[2]));
1064 $match[2] = check_plain($match[2]);
1065 return $match[1] . '<a href="http://' . $match[2] . '" title="' . $match[2] . '">' . $caption . '</a>' . $match[3];
1066 }
1067
1068 /**
1069 * Shortens long URLs to http://www.example.com/long/url...
1070 */
1071 function _filter_url_trim($text, $length = NULL) {
1072 static $_length;
1073 if ($length !== NULL) {
1074 $_length = $length;
1075 }
1076
1077 // Use +3 for '...' string length.
1078 if (strlen($text) > $_length + 3) {
1079 $text = substr($text, 0, $_length) . '...';
1080 }
1081
1082 return $text;
1083 }
1084
1085 /**
1086 * Filter tips callback for URL filter.
1087 */
1088 function _filter_url_tips($filter, $format, $long = FALSE) {
1089 return t('Web page addresses and e-mail addresses turn into links automatically.');
1090 }
1091
1092 /**
1093 * Scan input and make sure that all HTML tags are properly closed and nested.
1094 */
1095 function _filter_htmlcorrector($text) {
1096 return filter_dom_serialize(filter_dom_load($text));
1097 }
1098
1099 /**
1100 * Convert line breaks into <p> and <br> in an intelligent fashion.
1101 * Based on: http://photomatt.net/scripts/autop
1102 */
1103 function _filter_autop($text) {
1104 // All block level tags
1105 $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)';
1106
1107 // Split at <pre>, <script>, <style> and </pre>, </script>, </style> tags.
1108 // We don't apply any processing to the contents of these tags to avoid messing
1109 // up code. We look for matched pairs and allow basic nesting. For example:
1110 // "processed <pre> ignored <script> ignored </script> ignored </pre> processed"
1111 $chunks = preg_split('@(</?(?:pre|script|style|object)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
1112 // Note: PHP ensures the array consists of alternating delimiters and literals
1113 // and begins and ends with a literal (inserting NULL as required).
1114 $ignore = FALSE;
1115 $ignoretag = '';
1116 $output = '';
1117 foreach ($chunks as $i => $chunk) {
1118 if ($i % 2) {
1119 // Opening or closing tag?
1120 $open = ($chunk[1] != '/');
1121 list($tag) = preg_split('/[ >]/', substr($chunk, 2 - $open), 2);
1122 if (!$ignore) {
1123 if ($open) {
1124 $ignore = TRUE;
1125 $ignoretag =