Projet

Général

Profil

Paste
Télécharger (65,9 ko) Statistiques
| Branche: | Révision:

root / drupal7 / modules / filter / filter.module @ 6ff32cea

1
<?php
2

    
3
/**
4
 * @file
5
 * Framework for handling the filtering of content.
6
 */
7

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

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

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

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

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

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

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

    
152
/**
153
 * Loads a text format object from the database.
154
 *
155
 * @param $format_id
156
 *   The format ID.
157
 *
158
 * @return
159
 *   A fully-populated text format object, if the requested format exists and
160
 *   is enabled. If the format does not exist, or exists in the database but
161
 *   has been marked as disabled, FALSE is returned.
162
 *
163
 * @see filter_format_exists()
164
 */
165
function filter_format_load($format_id) {
166
  $formats = filter_formats();
167
  return isset($formats[$format_id]) ? $formats[$format_id] : FALSE;
168
}
169

    
170
/**
171
 * Saves a text format object to the database.
172
 *
173
 * @param $format
174
 *   A format object having the properties:
175
 *   - format: A machine-readable name representing the ID of the text format
176
 *     to save. If this corresponds to an existing text format, that format
177
 *     will be updated; otherwise, a new format will be created.
178
 *   - name: The title of the text format.
179
 *   - status: (optional) An integer indicating whether the text format is
180
 *     enabled (1) or not (0). Defaults to 1.
181
 *   - weight: (optional) The weight of the text format, which controls its
182
 *     placement in text format lists. If omitted, the weight is set to 0.
183
 *   - filters: (optional) An associative, multi-dimensional array of filters
184
 *     assigned to the text format, keyed by the name of each filter and using
185
 *     the properties:
186
 *     - weight: (optional) The weight of the filter in the text format. If
187
 *       omitted, either the currently stored weight is retained (if there is
188
 *       one), or the filter is assigned a weight of 10, which will usually
189
 *       put it at the bottom of the list.
190
 *     - status: (optional) A boolean indicating whether the filter is
191
 *       enabled in the text format. If omitted, the filter will be disabled.
192
 *     - settings: (optional) An array of configured settings for the filter.
193
 *       See hook_filter_info() for details.
194
 *
195
 * @return
196
 *   SAVED_NEW or SAVED_UPDATED.
197
 */
198
function filter_format_save($format) {
199
  $format->name = trim($format->name);
200
  $format->cache = _filter_format_is_cacheable($format);
201
  if (!isset($format->status)) {
202
    $format->status = 1;
203
  }
204
  if (!isset($format->weight)) {
205
    $format->weight = 0;
206
  }
207

    
208
  // Insert or update the text format.
209
  $return = db_merge('filter_format')
210
    ->key(array('format' => $format->format))
211
    ->fields(array(
212
      'name' => $format->name,
213
      'cache' => (int) $format->cache,
214
      'status' => (int) $format->status,
215
      'weight' => (int) $format->weight,
216
    ))
217
    ->execute();
218

    
219
  // Programmatic saves may not contain any filters.
220
  if (!isset($format->filters)) {
221
    $format->filters = array();
222
  }
223
  $filter_info = filter_get_filters();
224
  foreach ($filter_info as $name => $filter) {
225
    // If the format does not specify an explicit weight for a filter, assign
226
    // a default weight, either defined in hook_filter_info(), or the default of
227
    // 0 by filter_get_filters()
228
    if (!isset($format->filters[$name]['weight'])) {
229
      $format->filters[$name]['weight'] = $filter['weight'];
230
    }
231
    $format->filters[$name]['status'] = isset($format->filters[$name]['status']) ? $format->filters[$name]['status'] : 0;
232
    $format->filters[$name]['module'] = $filter['module'];
233

    
234
    // If settings were passed, only ensure default settings.
235
    if (isset($format->filters[$name]['settings'])) {
236
      if (isset($filter['default settings'])) {
237
        $format->filters[$name]['settings'] = array_merge($filter['default settings'], $format->filters[$name]['settings']);
238
      }
239
    }
240
    // Otherwise, use default settings or fall back to an empty array.
241
    else {
242
      $format->filters[$name]['settings'] = isset($filter['default settings']) ? $filter['default settings'] : array();
243
    }
244

    
245
    $fields = array();
246
    $fields['weight'] = $format->filters[$name]['weight'];
247
    $fields['status'] = $format->filters[$name]['status'];
248
    $fields['module'] = $format->filters[$name]['module'];
249
    $fields['settings'] = serialize($format->filters[$name]['settings']);
250

    
251
    db_merge('filter')
252
      ->key(array(
253
        'format' => $format->format,
254
        'name' => $name,
255
      ))
256
      ->fields($fields)
257
      ->execute();
258
  }
259

    
260
  if ($return == SAVED_NEW) {
261
    module_invoke_all('filter_format_insert', $format);
262
  }
263
  else {
264
    module_invoke_all('filter_format_update', $format);
265
    // Explicitly indicate that the format was updated. We need to do this
266
    // since if the filters were updated but the format object itself was not,
267
    // the merge query above would not return an indication that anything had
268
    // changed.
269
    $return = SAVED_UPDATED;
270

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

    
275
  filter_formats_reset();
276

    
277
  return $return;
278
}
279

    
280
/**
281
 * Disables a text format.
282
 *
283
 * There is no core facility to re-enable a disabled format. It is not deleted
284
 * to keep information for contrib and to make sure the format ID is never
285
 * reused. As there might be content using the disabled format, this would lead
286
 * to data corruption.
287
 *
288
 * @param $format
289
 *   The text format object to be disabled.
290
 */
291
function filter_format_disable($format) {
292
  db_update('filter_format')
293
    ->fields(array('status' => 0))
294
    ->condition('format', $format->format)
295
    ->execute();
296

    
297
  // Allow modules to react on text format deletion.
298
  module_invoke_all('filter_format_disable', $format);
299

    
300
  // Clear the filter cache whenever a text format is disabled.
301
  filter_formats_reset();
302
  cache_clear_all($format->format . ':', 'cache_filter', TRUE);
303
}
304

    
305
/**
306
 * Determines if a text format exists.
307
 *
308
 * @param $format_id
309
 *   The ID of the text format to check.
310
 *
311
 * @return
312
 *   TRUE if the text format exists, FALSE otherwise. Note that for disabled
313
 *   formats filter_format_exists() will return TRUE while filter_format_load()
314
 *   will return FALSE.
315
 *
316
 * @see filter_format_load()
317
 */
318
function filter_format_exists($format_id) {
319
  return (bool) db_query_range('SELECT 1 FROM {filter_format} WHERE format = :format', 0, 1, array(':format' => $format_id))->fetchField();
320
}
321

    
322
/**
323
 * Displays a text format form title.
324
 *
325
 * @param object $format
326
 *   A format object.
327
 *
328
 * @return string
329
 *   The name of the format.
330
 *
331
 * @see filter_menu()
332
 */
333
function filter_admin_format_title($format) {
334
  return $format->name;
335
}
336

    
337
/**
338
 * Implements hook_permission().
339
 */
340
function filter_permission() {
341
  $perms['administer filters'] = array(
342
    'title' => t('Administer text formats and filters'),
343
    'restrict access' => TRUE,
344
  );
345

    
346
  // Generate permissions for each text format. Warn the administrator that any
347
  // of them are potentially unsafe.
348
  foreach (filter_formats() as $format) {
349
    $permission = filter_permission_name($format);
350
    if (!empty($permission)) {
351
      $format_name_replacement = l($format->name, 'admin/config/content/formats/' . $format->format);
352
      $perms[$permission] = array(
353
        'title' => t("Use the !text_format text format", array('!text_format' => $format_name_replacement,)),
354
        'description' => drupal_placeholder(t('Warning: This permission may have security implications depending on how the text format is configured.')),
355
      );
356
    }
357
  }
358
  return $perms;
359
}
360

    
361
/**
362
 * Returns the machine-readable permission name for a provided text format.
363
 *
364
 * @param $format
365
 *   An object representing a text format.
366
 *
367
 * @return
368
 *   The machine-readable permission name, or FALSE if the provided text format
369
 *   is malformed or is the fallback format (which is available to all users).
370
 */
371
function filter_permission_name($format) {
372
  if (isset($format->format) && $format->format != filter_fallback_format()) {
373
    return 'use text format ' . $format->format;
374
  }
375
  return FALSE;
376
}
377

    
378
/**
379
 * Implements hook_modules_enabled().
380
 */
381
function filter_modules_enabled($modules) {
382
  // Reset the static cache of module-provided filters, in case any of the
383
  // newly enabled modules defines a new filter or alters existing ones.
384
  drupal_static_reset('filter_get_filters');
385
}
386

    
387
/**
388
 * Implements hook_modules_disabled().
389
 */
390
function filter_modules_disabled($modules) {
391
  // Reset the static cache of module-provided filters, in case any of the
392
  // newly disabled modules defined or altered any filters.
393
  drupal_static_reset('filter_get_filters');
394
}
395

    
396
/**
397
 * Retrieves a list of text formats, ordered by weight.
398
 *
399
 * @param $account
400
 *   (optional) If provided, only those formats that are allowed for this user
401
 *   account will be returned. All formats will be returned otherwise. Defaults
402
 *   to NULL.
403
 *
404
 * @return
405
 *   An array of text format objects, keyed by the format ID and ordered by
406
 *   weight.
407
 *
408
 * @see filter_formats_reset()
409
 */
410
function filter_formats($account = NULL) {
411
  global $language;
412
  $formats = &drupal_static(__FUNCTION__, array());
413

    
414
  // All available formats are cached for performance.
415
  if (!isset($formats['all'])) {
416
    if ($cache = cache_get("filter_formats:{$language->language}")) {
417
      $formats['all'] = $cache->data;
418
    }
419
    else {
420
      $formats['all'] = db_select('filter_format', 'ff')
421
        ->addTag('translatable')
422
        ->fields('ff')
423
        ->condition('status', 1)
424
        ->orderBy('weight')
425
        ->execute()
426
        ->fetchAllAssoc('format');
427

    
428
      cache_set("filter_formats:{$language->language}", $formats['all']);
429
    }
430
  }
431

    
432
  // Build a list of user-specific formats.
433
  if (isset($account) && !isset($formats['user'][$account->uid])) {
434
    $formats['user'][$account->uid] = array();
435
    foreach ($formats['all'] as $format) {
436
      if (filter_access($format, $account)) {
437
        $formats['user'][$account->uid][$format->format] = $format;
438
      }
439
    }
440
  }
441

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

    
445
/**
446
 * Resets the text format caches.
447
 *
448
 * @see filter_formats()
449
 */
450
function filter_formats_reset() {
451
  cache_clear_all('filter_formats', 'cache', TRUE);
452
  cache_clear_all('filter_list_format', 'cache', TRUE);
453
  drupal_static_reset('filter_list_format');
454
  drupal_static_reset('filter_formats');
455
}
456

    
457
/**
458
 * Retrieves a list of roles that are allowed to use a given text format.
459
 *
460
 * @param $format
461
 *   An object representing the text format.
462
 *
463
 * @return
464
 *   An array of role names, keyed by role ID.
465
 */
466
function filter_get_roles_by_format($format) {
467
  // Handle the fallback format upfront (all roles have access to this format).
468
  if ($format->format == filter_fallback_format()) {
469
    return user_roles();
470
  }
471
  // Do not list any roles if the permission does not exist.
472
  $permission = filter_permission_name($format);
473
  return !empty($permission) ? user_roles(FALSE, $permission) : array();
474
}
475

    
476
/**
477
 * Retrieves a list of text formats that are allowed for a given role.
478
 *
479
 * @param $rid
480
 *   The user role ID to retrieve text formats for.
481
 *
482
 * @return
483
 *   An array of text format objects that are allowed for the role, keyed by
484
 *   the text format ID and ordered by weight.
485
 */
486
function filter_get_formats_by_role($rid) {
487
  $formats = array();
488
  foreach (filter_formats() as $format) {
489
    $roles = filter_get_roles_by_format($format);
490
    if (isset($roles[$rid])) {
491
      $formats[$format->format] = $format;
492
    }
493
  }
494
  return $formats;
495
}
496

    
497
/**
498
 * Returns the ID of the default text format for a particular user.
499
 *
500
 * The default text format is the first available format that the user is
501
 * allowed to access, when the formats are ordered by weight. It should
502
 * generally be used as a default choice when presenting the user with a list
503
 * of possible text formats (for example, in a node creation form).
504
 *
505
 * Conversely, when existing content that does not have an assigned text format
506
 * needs to be filtered for display, the default text format is the wrong
507
 * choice, because it is not guaranteed to be consistent from user to user, and
508
 * some trusted users may have an unsafe text format set by default, which
509
 * should not be used on text of unknown origin. Instead, the fallback format
510
 * returned by filter_fallback_format() should be used, since that is intended
511
 * to be a safe, consistent format that is always available to all users.
512
 *
513
 * @param $account
514
 *   (optional) The user account to check. Defaults to the currently logged-in
515
 *   user. Defaults to NULL.
516
 *
517
 * @return
518
 *   The ID of the user's default text format.
519
 *
520
 * @see filter_fallback_format()
521
 */
522
function filter_default_format($account = NULL) {
523
  global $user;
524
  if (!isset($account)) {
525
    $account = $user;
526
  }
527
  // Get a list of formats for this user, ordered by weight. The first one
528
  // available is the user's default format.
529
  $formats = filter_formats($account);
530
  $format = reset($formats);
531
  return $format->format;
532
}
533

    
534
/**
535
 * Returns the ID of the fallback text format that all users have access to.
536
 *
537
 * The fallback text format is a regular text format in every respect, except
538
 * it does not participate in the filter permission system and cannot be
539
 * disabled. It needs to exist because any user who has permission to create
540
 * formatted content must always have at least one text format they can use.
541
 *
542
 * Because the fallback format is available to all users, it should always be
543
 * configured securely. For example, when the Filter module is installed, this
544
 * format is initialized to output plain text. Installation profiles and site
545
 * administrators have the freedom to configure it further.
546
 *
547
 * Note that the fallback format is completely distinct from the default format,
548
 * which differs per user and is simply the first format which that user has
549
 * access to. The default and fallback formats are only guaranteed to be the
550
 * same for users who do not have access to any other format; otherwise, the
551
 * fallback format's weight determines its placement with respect to the user's
552
 * other formats.
553
 *
554
 * Any modules implementing a format deletion functionality must not delete this
555
 * format.
556
 *
557
 * @return
558
 *   The ID of the fallback text format.
559
 *
560
 * @see hook_filter_format_disable()
561
 * @see filter_default_format()
562
 */
563
function filter_fallback_format() {
564
  // This variable is automatically set in the database for all installations
565
  // of Drupal. In the event that it gets disabled or deleted somehow, there
566
  // is no safe default to return, since we do not want to risk making an
567
  // existing (and potentially unsafe) text format on the site automatically
568
  // available to all users. Returning NULL at least guarantees that this
569
  // cannot happen.
570
  return variable_get('filter_fallback_format');
571
}
572

    
573
/**
574
 * Returns the title of the fallback text format.
575
 *
576
 * @return string
577
 *   The title of the fallback text format.
578
 */
579
function filter_fallback_format_title() {
580
  $fallback_format = filter_format_load(filter_fallback_format());
581
  return filter_admin_format_title($fallback_format);
582
}
583

    
584
/**
585
 * Returns a list of all filters provided by modules.
586
 *
587
 * @return array
588
 *   An array of filter formats.
589
 */
590
function filter_get_filters() {
591
  $filters = &drupal_static(__FUNCTION__, array());
592

    
593
  if (empty($filters)) {
594
    foreach (module_implements('filter_info') as $module) {
595
      $info = module_invoke($module, 'filter_info');
596
      if (isset($info) && is_array($info)) {
597
        // Assign the name of the module implementing the filters and ensure
598
        // default values.
599
        foreach (array_keys($info) as $name) {
600
          $info[$name]['module'] = $module;
601
          $info[$name] += array(
602
            'description' => '',
603
            'weight' => 0,
604
          );
605
        }
606
        $filters = array_merge($filters, $info);
607
      }
608
    }
609
    // Allow modules to alter filter definitions.
610
    drupal_alter('filter_info', $filters);
611

    
612
    uasort($filters, '_filter_list_cmp');
613
  }
614

    
615
  return $filters;
616
}
617

    
618
/**
619
 * Sorts an array of filters by filter name.
620
 *
621
 * Callback for uasort() within filter_get_filters().
622
 */
623
function _filter_list_cmp($a, $b) {
624
  return strcmp($a['title'], $b['title']);
625
}
626

    
627
/**
628
 * Checks if the text in a certain text format is allowed to be cached.
629
 *
630
 * This function can be used to check whether the result of the filtering
631
 * process can be cached. A text format may allow caching depending on the
632
 * filters enabled.
633
 *
634
 * @param $format_id
635
 *   The text format ID to check.
636
 *
637
 * @return
638
 *   TRUE if the given text format allows caching, FALSE otherwise.
639
 */
640
function filter_format_allowcache($format_id) {
641
  $format = filter_format_load($format_id);
642
  return !empty($format->cache);
643
}
644

    
645
/**
646
 * Helper function to determine whether the output of a given text format can be cached.
647
 *
648
 * The output of a given text format can be cached when all enabled filters in
649
 * the text format allow caching.
650
 *
651
 * @param $format
652
 *   The text format object to check.
653
 *
654
 * @return
655
 *   TRUE if all the filters enabled in the given text format allow caching,
656
 *   FALSE otherwise.
657
 *
658
 * @see filter_format_save()
659
 */
660
function _filter_format_is_cacheable($format) {
661
  if (empty($format->filters)) {
662
    return TRUE;
663
  }
664
  $filter_info = filter_get_filters();
665
  foreach ($format->filters as $name => $filter) {
666
    // By default, 'cache' is TRUE for all filters unless specified otherwise.
667
    if (!empty($filter['status']) && isset($filter_info[$name]['cache']) && !$filter_info[$name]['cache']) {
668
      return FALSE;
669
    }
670
  }
671
  return TRUE;
672
}
673

    
674
/**
675
 * Retrieves a list of filters for a given text format.
676
 *
677
 * Note that this function returns all associated filters regardless of whether
678
 * they are enabled or disabled. All functions working with the filter
679
 * information outside of filter administration should test for $filter->status
680
 * before performing actions with the filter.
681
 *
682
 * @param $format_id
683
 *   The format ID to retrieve filters for.
684
 *
685
 * @return
686
 *   An array of filter objects associated to the given text format, keyed by
687
 *   filter name.
688
 */
689
function filter_list_format($format_id) {
690
  $filters = &drupal_static(__FUNCTION__, array());
691
  $filter_info = filter_get_filters();
692

    
693
  if (!isset($filters['all'])) {
694
    if ($cache = cache_get('filter_list_format')) {
695
      $filters['all'] = $cache->data;
696
    }
697
    else {
698
      $result = db_query('SELECT * FROM {filter} ORDER BY weight, module, name');
699
      foreach ($result as $record) {
700
        $filters['all'][$record->format][$record->name] = $record;
701
      }
702
      cache_set('filter_list_format', $filters['all']);
703
    }
704
  }
705

    
706
  if (!isset($filters[$format_id])) {
707
    $format_filters = array();
708
    $filter_map = isset($filters['all'][$format_id]) ? $filters['all'][$format_id] : array();
709
    foreach ($filter_map as $name => $filter) {
710
      if (isset($filter_info[$name])) {
711
        $filter->title = $filter_info[$name]['title'];
712
        // Unpack stored filter settings.
713
        $filter->settings = (isset($filter->settings) ? unserialize($filter->settings) : array());
714
        // Merge in default settings.
715
        if (isset($filter_info[$name]['default settings'])) {
716
          $filter->settings += $filter_info[$name]['default settings'];
717
        }
718

    
719
        $format_filters[$name] = $filter;
720
      }
721
    }
722
    $filters[$format_id] = $format_filters;
723
  }
724

    
725
  return isset($filters[$format_id]) ? $filters[$format_id] : array();
726
}
727

    
728
/**
729
 * Runs all the enabled filters on a piece of text.
730
 *
731
 * Note: Because filters can inject JavaScript or execute PHP code, security is
732
 * vital here. When a user supplies a text format, you should validate it using
733
 * filter_access() before accepting/using it. This is normally done in the
734
 * validation stage of the Form API. You should for example never make a preview
735
 * of content in a disallowed format.
736
 *
737
 * @param $text
738
 *   The text to be filtered.
739
 * @param $format_id
740
 *   (optional) The machine name of the filter format to be used to filter the
741
 *   text. Defaults to the fallback format. See filter_fallback_format().
742
 * @param $langcode
743
 *   (optional) The language code of the text to be filtered, e.g. 'en' for
744
 *   English. This allows filters to be language aware so language specific
745
 *   text replacement can be implemented. Defaults to an empty string.
746
 * @param $cache
747
 *   (optional) A Boolean indicating whether to cache the filtered output in the
748
 *   {cache_filter} table. The caller may set this to FALSE when the output is
749
 *   already cached elsewhere to avoid duplicate cache lookups and storage.
750
 *   Defaults to FALSE.
751
 *
752
 * @return
753
 *   The filtered text.
754
 *
755
 * @ingroup sanitization
756
 */
757
function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) {
758
  if (!isset($format_id)) {
759
    $format_id = filter_fallback_format();
760
  }
761
  // If the requested text format does not exist, the text cannot be filtered.
762
  if (!$format = filter_format_load($format_id)) {
763
    watchdog('filter', 'Missing text format: %format.', array('%format' => $format_id), WATCHDOG_ALERT);
764
    return '';
765
  }
766

    
767
  // Check for a cached version of this piece of text.
768
  $cache = $cache && !empty($format->cache);
769
  $cache_id = '';
770
  if ($cache) {
771
    $cache_id = $format->format . ':' . $langcode . ':' . hash('sha256', $text);
772
    if ($cached = cache_get($cache_id, 'cache_filter')) {
773
      return $cached->data;
774
    }
775
  }
776

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

    
781
  // Get a complete list of filters, ordered properly.
782
  $filters = filter_list_format($format->format);
783
  $filter_info = filter_get_filters();
784

    
785
  // Give filters the chance to escape HTML-like data such as code or formulas.
786
  foreach ($filters as $name => $filter) {
787
    if ($filter->status && isset($filter_info[$name]['prepare callback']) && function_exists($filter_info[$name]['prepare callback'])) {
788
      $function = $filter_info[$name]['prepare callback'];
789
      $text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
790
    }
791
  }
792

    
793
  // Perform filtering.
794
  foreach ($filters as $name => $filter) {
795
    if ($filter->status && isset($filter_info[$name]['process callback']) && function_exists($filter_info[$name]['process callback'])) {
796
      $function = $filter_info[$name]['process callback'];
797
      $text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
798
    }
799
  }
800

    
801
  // Cache the filtered text. This cache is infinitely valid. It becomes
802
  // obsolete when $text changes (which leads to a new $cache_id). It is
803
  // automatically flushed when the text format is updated.
804
  // @see filter_format_save()
805
  if ($cache) {
806
    cache_set($cache_id, $text, 'cache_filter');
807
  }
808

    
809
  return $text;
810
}
811

    
812
/**
813
 * Expands an element into a base element with text format selector attached.
814
 *
815
 * The form element will be expanded into two separate form elements, one
816
 * holding the original element, and the other holding the text format selector:
817
 * - value: Holds the original element, having its #type changed to the value of
818
 *   #base_type or 'textarea' by default.
819
 * - format: Holds the text format fieldset and the text format selection, using
820
 *   the text format id specified in #format or the user's default format by
821
 *   default, if NULL.
822
 *
823
 * The resulting value for the element will be an array holding the value and
824
 * the format. For example, the value for the body element will be:
825
 * @code
826
 *   $form_state['values']['body']['value'] = 'foo';
827
 *   $form_state['values']['body']['format'] = 'foo';
828
 * @endcode
829
 *
830
 * @param $element
831
 *   The form element to process. Properties used:
832
 *   - #base_type: The form element #type to use for the 'value' element.
833
 *     'textarea' by default.
834
 *   - #format: (optional) The text format ID to preselect. If NULL or not set,
835
 *     the default format for the current user will be used.
836
 *
837
 * @return
838
 *   The expanded element.
839
 */
840
function filter_process_format($element) {
841
  global $user;
842

    
843
  // Ensure that children appear as subkeys of this element.
844
  $element['#tree'] = TRUE;
845
  $blacklist = array(
846
    // Make form_builder() regenerate child properties.
847
    '#parents',
848
    '#id',
849
    '#name',
850
    // Do not copy this #process function to prevent form_builder() from
851
    // recursing infinitely.
852
    '#process',
853
    // Description is handled by theme_text_format_wrapper().
854
    '#description',
855
    // Ensure proper ordering of children.
856
    '#weight',
857
    // Properties already processed for the parent element.
858
    '#prefix',
859
    '#suffix',
860
    '#attached',
861
    '#processed',
862
    '#theme_wrappers',
863
  );
864
  // Move this element into sub-element 'value'.
865
  unset($element['value']);
866
  foreach (element_properties($element) as $key) {
867
    if (!in_array($key, $blacklist)) {
868
      $element['value'][$key] = $element[$key];
869
    }
870
  }
871

    
872
  $element['value']['#type'] = $element['#base_type'];
873
  $element['value'] += element_info($element['#base_type']);
874

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

    
880
  // Setup child container for the text format widget.
881
  $element['format'] = array(
882
    '#type' => 'fieldset',
883
    '#attributes' => array('class' => array('filter-wrapper')),
884
  );
885

    
886
  // Prepare text format guidelines.
887
  $element['format']['guidelines'] = array(
888
    '#type' => 'container',
889
    '#attributes' => array('class' => array('filter-guidelines')),
890
    '#weight' => 20,
891
  );
892
  // Get a list of formats that the current user has access to.
893
  $formats = filter_formats($user);
894
  foreach ($formats as $format) {
895
    $options[$format->format] = $format->name;
896
    $element['format']['guidelines'][$format->format] = array(
897
      '#theme' => 'filter_guidelines',
898
      '#format' => $format,
899
    );
900
  }
901

    
902
  // Use the default format for this user if none was selected.
903
  if (!isset($element['#format'])) {
904
    $element['#format'] = filter_default_format($user);
905
  }
906

    
907
  $element['format']['format'] = array(
908
    '#type' => 'select',
909
    '#title' => t('Text format'),
910
    '#options' => $options,
911
    '#default_value' => $element['#format'],
912
    '#access' => count($formats) > 1,
913
    '#weight' => 10,
914
    '#attributes' => array('class' => array('filter-list')),
915
    '#parents' => array_merge($element['#parents'], array('format')),
916
  );
917

    
918
  $element['format']['help'] = array(
919
    '#type' => 'container',
920
    '#theme' => 'filter_tips_more_info',
921
    '#attributes' => array('class' => array('filter-help')),
922
    '#weight' => 0,
923
  );
924

    
925
  $all_formats = filter_formats();
926
  $format_exists = isset($all_formats[$element['#format']]);
927
  $user_has_access = isset($formats[$element['#format']]);
928
  $user_is_admin = user_access('administer filters');
929

    
930
  // If the stored format does not exist, administrators have to assign a new
931
  // format.
932
  if (!$format_exists && $user_is_admin) {
933
    $element['format']['format']['#required'] = TRUE;
934
    $element['format']['format']['#default_value'] = NULL;
935
    // Force access to the format selector (it may have been denied above if
936
    // the user only has access to a single format).
937
    $element['format']['format']['#access'] = TRUE;
938
  }
939
  // Disable this widget, if the user is not allowed to use the stored format,
940
  // or if the stored format does not exist. The 'administer filters' permission
941
  // only grants access to the filter administration, not to all formats.
942
  elseif (!$user_has_access || !$format_exists) {
943
    // Overload default values into #value to make them unalterable.
944
    $element['value']['#value'] = $element['value']['#default_value'];
945
    $element['format']['format']['#value'] = $element['format']['format']['#default_value'];
946

    
947
    // Prepend #pre_render callback to replace field value with user notice
948
    // prior to rendering.
949
    $element['value'] += array('#pre_render' => array());
950
    array_unshift($element['value']['#pre_render'], 'filter_form_access_denied');
951

    
952
    // Cosmetic adjustments.
953
    if (isset($element['value']['#rows'])) {
954
      $element['value']['#rows'] = 3;
955
    }
956
    $element['value']['#disabled'] = TRUE;
957
    $element['value']['#resizable'] = FALSE;
958

    
959
    // Hide the text format selector and any other child element (such as text
960
    // field's summary).
961
    foreach (element_children($element) as $key) {
962
      if ($key != 'value') {
963
        $element[$key]['#access'] = FALSE;
964
      }
965
    }
966
  }
967

    
968
  return $element;
969
}
970

    
971
/**
972
 * Render API callback: Hides the field value of 'text_format' elements.
973
 *
974
 * To not break form processing and previews if a user does not have access to a
975
 * stored text format, the expanded form elements in filter_process_format() are
976
 * forced to take over the stored #default_values for 'value' and 'format'.
977
 * However, to prevent the unfiltered, original #value from being displayed to
978
 * the user, we replace it with a friendly notice here.
979
 *
980
 * @see filter_process_format()
981
 */
982
function filter_form_access_denied($element) {
983
  $element['#value'] = t('This field has been disabled because you do not have sufficient permissions to edit it.');
984
  return $element;
985
}
986

    
987
/**
988
 * Returns HTML for a text format-enabled form element.
989
 *
990
 * @param $variables
991
 *   An associative array containing:
992
 *   - element: A render element containing #children and #description.
993
 *
994
 * @ingroup themeable
995
 */
996
function theme_text_format_wrapper($variables) {
997
  $element = $variables['element'];
998
  $output = '<div class="text-format-wrapper">';
999
  $output .= $element['#children'];
1000
  if (!empty($element['#description'])) {
1001
    $output .= '<div class="description">' . $element['#description'] . '</div>';
1002
  }
1003
  $output .= "</div>\n";
1004

    
1005
  return $output;
1006
}
1007

    
1008
/**
1009
 * Checks if a user has access to a particular text format.
1010
 *
1011
 * @param $format
1012
 *   An object representing the text format.
1013
 * @param $account
1014
 *   (optional) The user account to check access for; if omitted, the currently
1015
 *   logged-in user is used. Defaults to NULL.
1016
 *
1017
 * @return
1018
 *   Boolean TRUE if the user is allowed to access the given format.
1019
 */
1020
function filter_access($format, $account = NULL) {
1021
  global $user;
1022
  if (!isset($account)) {
1023
    $account = $user;
1024
  }
1025
  // Handle special cases up front. All users have access to the fallback
1026
  // format.
1027
  if ($format->format == filter_fallback_format()) {
1028
    return TRUE;
1029
  }
1030
  // Check the permission if one exists; otherwise, we have a non-existent
1031
  // format so we return FALSE.
1032
  $permission = filter_permission_name($format);
1033
  return !empty($permission) && user_access($permission, $account);
1034
}
1035

    
1036
/**
1037
 * Retrieves the filter tips.
1038
 *
1039
 * @param $format_id
1040
 *   The ID of the text format for which to retrieve tips, or -1 to return tips
1041
 *   for all formats accessible to the current user.
1042
 * @param $long
1043
 *   (optional) Boolean indicating whether the long form of tips should be
1044
 *   returned. Defaults to FALSE.
1045
 *
1046
 * @return
1047
 *   An associative array of filtering tips, keyed by filter name. Each
1048
 *   filtering tip is an associative array with elements:
1049
 *   - tip: Tip text.
1050
 *   - id: Filter ID.
1051
 */
1052
function _filter_tips($format_id, $long = FALSE) {
1053
  global $user;
1054

    
1055
  $formats = filter_formats($user);
1056
  $filter_info = filter_get_filters();
1057

    
1058
  $tips = array();
1059

    
1060
  // If only listing one format, extract it from the $formats array.
1061
  if ($format_id != -1) {
1062
    $formats = array($formats[$format_id]);
1063
  }
1064

    
1065
  foreach ($formats as $format) {
1066
    $filters = filter_list_format($format->format);
1067
    $tips[$format->name] = array();
1068
    foreach ($filters as $name => $filter) {
1069
      if ($filter->status && isset($filter_info[$name]['tips callback']) && function_exists($filter_info[$name]['tips callback'])) {
1070
        $tip = $filter_info[$name]['tips callback']($filter, $format, $long);
1071
        if (isset($tip)) {
1072
          $tips[$format->name][$name] = array('tip' => $tip, 'id' => $name);
1073
        }
1074
      }
1075
    }
1076
  }
1077

    
1078
  return $tips;
1079
}
1080

    
1081
/**
1082
 * Parses an HTML snippet and returns it as a DOM object.
1083
 *
1084
 * This function loads the body part of a partial (X)HTML document and returns
1085
 * a full DOMDocument object that represents this document. You can use
1086
 * filter_dom_serialize() to serialize this DOMDocument back to a XHTML
1087
 * snippet.
1088
 *
1089
 * @param $text
1090
 *   The partial (X)HTML snippet to load. Invalid mark-up will be corrected on
1091
 *   import.
1092
 * @return
1093
 *   A DOMDocument that represents the loaded (X)HTML snippet.
1094
 */
1095
function filter_dom_load($text) {
1096
  $dom_document = new DOMDocument();
1097
  // Ignore warnings during HTML soup loading.
1098
  @$dom_document->loadHTML('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $text . '</body></html>');
1099

    
1100
  return $dom_document;
1101
}
1102

    
1103
/**
1104
 * Converts a DOM object back to an HTML snippet.
1105
 *
1106
 * The function serializes the body part of a DOMDocument back to an XHTML
1107
 * snippet. The resulting XHTML snippet will be properly formatted to be
1108
 * compatible with HTML user agents.
1109
 *
1110
 * @param $dom_document
1111
 *   A DOMDocument object to serialize, only the tags below
1112
 *   the first <body> node will be converted.
1113
 *
1114
 * @return
1115
 *   A valid (X)HTML snippet, as a string.
1116
 */
1117
function filter_dom_serialize($dom_document) {
1118
  $body_node = $dom_document->getElementsByTagName('body')->item(0);
1119
  $body_content = '';
1120

    
1121
  foreach ($body_node->getElementsByTagName('script') as $node) {
1122
    filter_dom_serialize_escape_cdata_element($dom_document, $node);
1123
  }
1124

    
1125
  foreach ($body_node->getElementsByTagName('style') as $node) {
1126
    filter_dom_serialize_escape_cdata_element($dom_document, $node, '/*', '*/');
1127
  }
1128

    
1129
  foreach ($body_node->childNodes as $child_node) {
1130
    $body_content .= $dom_document->saveXML($child_node);
1131
  }
1132
  return preg_replace('|<([^> ]*)/>|i', '<$1 />', $body_content);
1133
}
1134

    
1135
/**
1136
 * Adds comments around the <!CDATA section in a dom element.
1137
 *
1138
 * DOMDocument::loadHTML in filter_dom_load() makes CDATA sections from the
1139
 * contents of inline script and style tags.  This can cause HTML 4 browsers to
1140
 * throw exceptions.
1141
 *
1142
 * This function attempts to solve the problem by creating a DocumentFragment
1143
 * and imitating the behavior in drupal_get_js(), commenting the CDATA tag.
1144
 *
1145
 * @param $dom_document
1146
 *   The DOMDocument containing the $dom_element.
1147
 * @param $dom_element
1148
 *   The element potentially containing a CDATA node.
1149
 * @param $comment_start
1150
 *   (optional) A string to use as a comment start marker to escape the CDATA
1151
 *   declaration. Defaults to '//'.
1152
 * @param $comment_end
1153
 *   (optional) A string to use as a comment end marker to escape the CDATA
1154
 *   declaration. Defaults to an empty string.
1155
 */
1156
function filter_dom_serialize_escape_cdata_element($dom_document, $dom_element, $comment_start = '//', $comment_end = '') {
1157
  foreach ($dom_element->childNodes as $node) {
1158
    if (get_class($node) == 'DOMCdataSection') {
1159
      // See drupal_get_js().  This code is more or less duplicated there.
1160
      $embed_prefix = "\n<!--{$comment_start}--><![CDATA[{$comment_start} ><!--{$comment_end}\n";
1161
      $embed_suffix = "\n{$comment_start}--><!]]>{$comment_end}\n";
1162

    
1163
      // Prevent invalid cdata escaping as this would throw a DOM error.
1164
      // This is the same behavior as found in libxml2.
1165
      // Related W3C standard: http://www.w3.org/TR/REC-xml/#dt-cdsection
1166
      // Fix explanation: http://en.wikipedia.org/wiki/CDATA#Nesting
1167
      $data = str_replace(']]>', ']]]]><![CDATA[>', $node->data);
1168

    
1169
      $fragment = $dom_document->createDocumentFragment();
1170
      $fragment->appendXML($embed_prefix . $data . $embed_suffix);
1171
      $dom_element->appendChild($fragment);
1172
      $dom_element->removeChild($node);
1173
    }
1174
  }
1175
}
1176

    
1177
/**
1178
 * Returns HTML for a link to the more extensive filter tips.
1179
 *
1180
 * @ingroup themeable
1181
 */
1182
function theme_filter_tips_more_info() {
1183
  return '<p>' . l(t('More information about text formats'), 'filter/tips', array('attributes' => array('target' => '_blank'))) . '</p>';
1184
}
1185

    
1186
/**
1187
 * Returns HTML for guidelines for a text format.
1188
 *
1189
 * @param $variables
1190
 *   An associative array containing:
1191
 *   - format: An object representing a text format.
1192
 *
1193
 * @ingroup themeable
1194
 */
1195
function theme_filter_guidelines($variables) {
1196
  $format = $variables['format'];
1197
  $attributes['class'][] = 'filter-guidelines-item';
1198
  $attributes['class'][] = 'filter-guidelines-' . $format->format;
1199
  $output = '<div' . drupal_attributes($attributes) . '>';
1200
  $output .= '<h3>' . check_plain($format->name) . '</h3>';
1201
  $output .= theme('filter_tips', array('tips' => _filter_tips($format->format, FALSE)));
1202
  $output .= '</div>';
1203
  return $output;
1204
}
1205

    
1206
/**
1207
 * @defgroup standard_filters Standard filters
1208
 * @{
1209
 * Filters implemented by the Filter module.
1210
 */
1211

    
1212
/**
1213
 * Implements hook_filter_info().
1214
 */
1215
function filter_filter_info() {
1216
  $filters['filter_html'] = array(
1217
    'title' => t('Limit allowed HTML tags'),
1218
    'process callback' => '_filter_html',
1219
    'settings callback' => '_filter_html_settings',
1220
    'default settings' => array(
1221
      'allowed_html' => '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>',
1222
      'filter_html_help' => 1,
1223
      'filter_html_nofollow' => 0,
1224
    ),
1225
    'tips callback' => '_filter_html_tips',
1226
    'weight' => -10,
1227
  );
1228
  $filters['filter_autop'] = array(
1229
    'title' => t('Convert line breaks into HTML (i.e. <code>&lt;br&gt;</code> and <code>&lt;p&gt;</code>)'),
1230
    'process callback' => '_filter_autop',
1231
    'tips callback' => '_filter_autop_tips',
1232
  );
1233
  $filters['filter_url'] = array(
1234
    'title' => t('Convert URLs into links'),
1235
    'process callback' => '_filter_url',
1236
    'settings callback' => '_filter_url_settings',
1237
    'default settings' => array(
1238
      'filter_url_length' => 72,
1239
    ),
1240
    'tips callback' => '_filter_url_tips',
1241
  );
1242
  $filters['filter_htmlcorrector'] = array(
1243
    'title' =>  t('Correct faulty and chopped off HTML'),
1244
    'process callback' => '_filter_htmlcorrector',
1245
    'weight' => 10,
1246
  );
1247
  $filters['filter_html_escape'] = array(
1248
    'title' => t('Display any HTML as plain text'),
1249
    'process callback' => '_filter_html_escape',
1250
    'tips callback' => '_filter_html_escape_tips',
1251
    'weight' => -10,
1252
  );
1253
  return $filters;
1254
}
1255

    
1256
/**
1257
 * Implements callback_filter_settings().
1258
 *
1259
 * Filter settings callback for the HTML content filter.
1260
 */
1261
function _filter_html_settings($form, &$form_state, $filter, $format, $defaults) {
1262
  $filter->settings += $defaults;
1263

    
1264
  $settings['allowed_html'] = array(
1265
    '#type' => 'textfield',
1266
    '#title' => t('Allowed HTML tags'),
1267
    '#default_value' => $filter->settings['allowed_html'],
1268
    '#maxlength' => 1024,
1269
    '#description' => t('A list of HTML tags that can be used. JavaScript event attributes, JavaScript URLs, and CSS are always stripped.'),
1270
  );
1271
  $settings['filter_html_help'] = array(
1272
    '#type' => 'checkbox',
1273
    '#title' => t('Display basic HTML help in long filter tips'),
1274
    '#default_value' => $filter->settings['filter_html_help'],
1275
  );
1276
  $settings['filter_html_nofollow'] = array(
1277
    '#type' => 'checkbox',
1278
    '#title' => t('Add rel="nofollow" to all links'),
1279
    '#default_value' => $filter->settings['filter_html_nofollow'],
1280
  );
1281
  return $settings;
1282
}
1283

    
1284
/**
1285
 * Implements callback_filter_process().
1286
 *
1287
 * Provides filtering of input into accepted HTML.
1288
 */
1289
function _filter_html($text, $filter) {
1290
  $allowed_tags = preg_split('/\s+|<|>/', $filter->settings['allowed_html'], -1, PREG_SPLIT_NO_EMPTY);
1291
  $text = filter_xss($text, $allowed_tags);
1292

    
1293
  if ($filter->settings['filter_html_nofollow']) {
1294
    $html_dom = filter_dom_load($text);
1295
    $links = $html_dom->getElementsByTagName('a');
1296
    foreach ($links as $link) {
1297
      $link->setAttribute('rel', 'nofollow');
1298
    }
1299
    $text = filter_dom_serialize($html_dom);
1300
  }
1301

    
1302
  return trim($text);
1303
}
1304

    
1305
/**
1306
 * Implements callback_filter_tips().
1307
 *
1308
 * Provides help for the HTML filter.
1309
 *
1310
 * @see filter_filter_info()
1311
 */
1312
function _filter_html_tips($filter, $format, $long = FALSE) {
1313
  global $base_url;
1314

    
1315
  if (!($allowed_html = $filter->settings['allowed_html'])) {
1316
    return;
1317
  }
1318
  $output = t('Allowed HTML tags: @tags', array('@tags' => $allowed_html));
1319
  if (!$long) {
1320
    return $output;
1321
  }
1322

    
1323
  $output = '<p>' . $output . '</p>';
1324
  if (!$filter->settings['filter_html_help']) {
1325
    return $output;
1326
  }
1327

    
1328
  $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>';
1329
  $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>';
1330
  $tips = array(
1331
    'a' => array(t('Anchors are used to make links to other pages.'), '<a href="' . $base_url . '">' . check_plain(variable_get('site_name', 'Drupal')) . '</a>'),
1332
    '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')),
1333
    '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>'),
1334
    'strong' => array(t('Strong', array(), array('context' => 'Font weight')), '<strong>' . t('Strong', array(), array('context' => 'Font weight')) . '</strong>'),
1335
    'em' => array(t('Emphasized'), '<em>' . t('Emphasized') . '</em>'),
1336
    'cite' => array(t('Cited'), '<cite>' . t('Cited') . '</cite>'),
1337
    'code' => array(t('Coded text used to show programming source code'), '<code>' . t('Coded') . '</code>'),
1338
    'b' => array(t('Bolded'), '<b>' . t('Bolded') . '</b>'),
1339
    'u' => array(t('Underlined'), '<u>' . t('Underlined') . '</u>'),
1340
    'i' => array(t('Italicized'), '<i>' . t('Italicized') . '</i>'),
1341
    'sup' => array(t('Superscripted'), t('<sup>Super</sup>scripted')),
1342
    'sub' => array(t('Subscripted'), t('<sub>Sub</sub>scripted')),
1343
    'pre' => array(t('Preformatted'), '<pre>' . t('Preformatted') . '</pre>'),
1344
    'abbr' => array(t('Abbreviation'), t('<abbr title="Abbreviation">Abbrev.</abbr>')),
1345
    'acronym' => array(t('Acronym'), t('<acronym title="Three-Letter Acronym">TLA</acronym>')),
1346
    'blockquote' => array(t('Block quoted'), '<blockquote>' . t('Block quoted') . '</blockquote>'),
1347
    'q' => array(t('Quoted inline'), '<q>' . t('Quoted inline') . '</q>'),
1348
    // Assumes and describes tr, td, th.
1349
    'table' => array(t('Table'), '<table> <tr><th>' . t('Table header') . '</th></tr> <tr><td>' . t('Table cell') . '</td></tr> </table>'),
1350
    'tr' => NULL, 'td' => NULL, 'th' => NULL,
1351
    'del' => array(t('Deleted'), '<del>' . t('Deleted') . '</del>'),
1352
    'ins' => array(t('Inserted'), '<ins>' . t('Inserted') . '</ins>'),
1353
     // Assumes and describes li.
1354
    '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>'),
1355
    '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>'),
1356
    'li' => NULL,
1357
    // Assumes and describes dt and dd.
1358
    '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>'),
1359
    'dt' => NULL, 'dd' => NULL,
1360
    'h1' => array(t('Heading'), '<h1>' . t('Title') . '</h1>'),
1361
    'h2' => array(t('Heading'), '<h2>' . t('Subtitle') . '</h2>'),
1362
    'h3' => array(t('Heading'), '<h3>' . t('Subtitle three') . '</h3>'),
1363
    'h4' => array(t('Heading'), '<h4>' . t('Subtitle four') . '</h4>'),
1364
    'h5' => array(t('Heading'), '<h5>' . t('Subtitle five') . '</h5>'),
1365
    'h6' => array(t('Heading'), '<h6>' . t('Subtitle six') . '</h6>')
1366
  );
1367
  $header = array(t('Tag Description'), t('You Type'), t('You Get'));
1368
  preg_match_all('/<([a-z0-9]+)[^a-z0-9]/i', $allowed_html, $out);
1369
  foreach ($out[1] as $tag) {
1370
    if (!empty($tips[$tag])) {
1371
      $rows[] = array(
1372
        array('data' => $tips[$tag][0], 'class' => array('description')),
1373
        array('data' => '<code>' . check_plain($tips[$tag][1]) . '</code>', 'class' => array('type')),
1374
        array('data' => $tips[$tag][1], 'class' => array('get'))
1375
      );
1376
    }
1377
    else {
1378
      $rows[] = array(
1379
        array('data' => t('No help provided for tag %tag.', array('%tag' => $tag)), 'class' => array('description'), 'colspan' => 3),
1380
      );
1381
    }
1382
  }
1383
  $output .= theme('table', array('header' => $header, 'rows' => $rows));
1384

    
1385
  $output .= '<p>' . t('Most unusual characters can be directly entered without any problems.') . '</p>';
1386
  $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>';
1387

    
1388
  $entities = array(
1389
    array(t('Ampersand'), '&amp;'),
1390
    array(t('Greater than'), '&gt;'),
1391
    array(t('Less than'), '&lt;'),
1392
    array(t('Quotation mark'), '&quot;'),
1393
  );
1394
  $header = array(t('Character Description'), t('You Type'), t('You Get'));
1395
  unset($rows);
1396
  foreach ($entities as $entity) {
1397
    $rows[] = array(
1398
      array('data' => $entity[0], 'class' => array('description')),
1399
      array('data' => '<code>' . check_plain($entity[1]) . '</code>', 'class' => array('type')),
1400
      array('data' => $entity[1], 'class' => array('get'))
1401
    );
1402
  }
1403
  $output .= theme('table', array('header' => $header, 'rows' => $rows));
1404
  return $output;
1405
}
1406

    
1407
/**
1408
 * Implements callback_filter_settings().
1409
 *
1410
 * Provides settings for the URL filter.
1411
 *
1412
 * @see filter_filter_info()
1413
 */
1414
function _filter_url_settings($form, &$form_state, $filter, $format, $defaults) {
1415
  $filter->settings += $defaults;
1416

    
1417
  $settings['filter_url_length'] = array(
1418
    '#type' => 'textfield',
1419
    '#title' => t('Maximum link text length'),
1420
    '#default_value' => $filter->settings['filter_url_length'],
1421
    '#size' => 5,
1422
    '#maxlength' => 4,
1423
    '#field_suffix' => t('characters'),
1424
    '#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.'),
1425
    '#element_validate' => array('element_validate_integer_positive'),
1426
  );
1427
  return $settings;
1428
}
1429

    
1430
/**
1431
 * Implements callback_filter_process().
1432
 *
1433
 * Converts text into hyperlinks automatically.
1434
 *
1435
 * This filter identifies and makes clickable three types of "links".
1436
 * - URLs like http://example.com.
1437
 * - E-mail addresses like name@example.com.
1438
 * - Web addresses without the "http://" protocol defined, like www.example.com.
1439
 * Each type must be processed separately, as there is no one regular
1440
 * expression that could possibly match all of the cases in one pass.
1441
 */
1442
function _filter_url($text, $filter) {
1443
  // Tags to skip and not recurse into.
1444
  $ignore_tags = 'a|script|style|code|pre';
1445

    
1446
  // Pass length to regexp callback.
1447
  _filter_url_trim(NULL, $filter->settings['filter_url_length']);
1448

    
1449
  // Create an array which contains the regexps for each type of link.
1450
  // The key to the regexp is the name of a function that is used as
1451
  // callback function to process matches of the regexp. The callback function
1452
  // is to return the replacement for the match. The array is used and
1453
  // matching/replacement done below inside some loops.
1454
  $tasks = array();
1455

    
1456
  // Prepare protocols pattern for absolute URLs.
1457
  // check_url() will replace any bad protocols with HTTP, so we need to support
1458
  // the identical list. While '//' is technically optional for MAILTO only,
1459
  // we cannot cleanly differ between protocols here without hard-coding MAILTO,
1460
  // so '//' is optional for all protocols.
1461
  // @see filter_xss_bad_protocol()
1462
  $protocols = variable_get('filter_allowed_protocols', array('ftp', 'http', 'https', 'irc', 'mailto', 'news', 'nntp', 'rtsp', 'sftp', 'ssh', 'tel', 'telnet', 'webcal'));
1463
  $protocols = implode(':(?://)?|', $protocols) . ':(?://)?';
1464

    
1465
  // Prepare domain name pattern.
1466
  // The ICANN seems to be on track towards accepting more diverse top level
1467
  // domains, so this pattern has been "future-proofed" to allow for TLDs
1468
  // of length 2-64.
1469
  $domain = '(?:[A-Za-z0-9._+-]+\.)?[A-Za-z]{2,64}\b';
1470
  $ip = '(?:[0-9]{1,3}\.){3}[0-9]{1,3}';
1471
  $auth = '[a-zA-Z0-9:%_+*~#?&=.,/;-]+@';
1472
  $trail = '[a-zA-Z0-9:%_+*~#&\[\]=/;?!\.,-]*[a-zA-Z0-9:%_+*~#&\[\]=/;-]';
1473

    
1474
  // Prepare pattern for optional trailing punctuation.
1475
  // Even these characters could have a valid meaning for the URL, such usage is
1476
  // rare compared to using a URL at the end of or within a sentence, so these
1477
  // trailing characters are optionally excluded.
1478
  $punctuation = '[\.,?!]*?';
1479

    
1480
  // Match absolute URLs.
1481
  $url_pattern = "(?:$auth)?(?:$domain|$ip)/?(?:$trail)?";
1482
  $pattern = "`((?:$protocols)(?:$url_pattern))($punctuation)`";
1483
  $tasks['_filter_url_parse_full_links'] = $pattern;
1484

    
1485
  // Match e-mail addresses.
1486
  $url_pattern = "[A-Za-z0-9._-]{1,254}@(?:$domain)";
1487
  $pattern = "`($url_pattern)`";
1488
  $tasks['_filter_url_parse_email_links'] = $pattern;
1489

    
1490
  // Match www domains.
1491
  $url_pattern = "www\.(?:$domain)/?(?:$trail)?";
1492
  $pattern = "`($url_pattern)($punctuation)`";
1493
  $tasks['_filter_url_parse_partial_links'] = $pattern;
1494

    
1495
  // Each type of URL needs to be processed separately. The text is joined and
1496
  // re-split after each task, since all injected HTML tags must be correctly
1497
  // protected before the next task.
1498
  foreach ($tasks as $task => $pattern) {
1499
    // HTML comments need to be handled separately, as they may contain HTML
1500
    // markup, especially a '>'. Therefore, remove all comment contents and add
1501
    // them back later.
1502
    _filter_url_escape_comments('', TRUE);
1503
    $text = preg_replace_callback('`<!--(.*?)-->`s', '_filter_url_escape_comments', $text);
1504

    
1505
    // Split at all tags; ensures that no tags or attributes are processed.
1506
    $chunks = preg_split('/(<.+?>)/is', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
1507
    // PHP ensures that the array consists of alternating delimiters and
1508
    // literals, and begins and ends with a literal (inserting NULL as
1509
    // required). Therefore, the first chunk is always text:
1510
    $chunk_type = 'text';
1511
    // If a tag of $ignore_tags is found, it is stored in $open_tag and only
1512
    // removed when the closing tag is found. Until the closing tag is found,
1513
    // no replacements are made.
1514
    $open_tag = '';
1515

    
1516
    for ($i = 0; $i < count($chunks); $i++) {
1517
      if ($chunk_type == 'text') {
1518
        // Only process this text if there are no unclosed $ignore_tags.
1519
        if ($open_tag == '') {
1520
          // If there is a match, inject a link into this chunk via the callback
1521
          // function contained in $task.
1522
          $chunks[$i] = preg_replace_callback($pattern, $task, $chunks[$i]);
1523
        }
1524
        // Text chunk is done, so next chunk must be a tag.
1525
        $chunk_type = 'tag';
1526
      }
1527
      else {
1528
        // Only process this tag if there are no unclosed $ignore_tags.
1529
        if ($open_tag == '') {
1530
          // Check whether this tag is contained in $ignore_tags.
1531
          if (preg_match("`<($ignore_tags)(?:\s|>)`i", $chunks[$i], $matches)) {
1532
            $open_tag = $matches[1];
1533
          }
1534
        }
1535
        // Otherwise, check whether this is the closing tag for $open_tag.
1536
        else {
1537
          if (preg_match("`<\/$open_tag>`i", $chunks[$i], $matches)) {
1538
            $open_tag = '';
1539
          }
1540
        }
1541
        // Tag chunk is done, so next chunk must be text.
1542
        $chunk_type = 'text';
1543
      }
1544
    }
1545

    
1546
    $text = implode($chunks);
1547
    // Revert back to the original comment contents
1548
    _filter_url_escape_comments('', FALSE);
1549
    $text = preg_replace_callback('`<!--(.*?)-->`', '_filter_url_escape_comments', $text);
1550
  }
1551

    
1552
  return $text;
1553
}
1554

    
1555
/**
1556
 * Makes links out of absolute URLs.
1557
 *
1558
 * Callback for preg_replace_callback() within _filter_url().
1559
 */
1560
function _filter_url_parse_full_links($match) {
1561
  // The $i:th parenthesis in the regexp contains the URL.
1562
  $i = 1;
1563

    
1564
  $match[$i] = decode_entities($match[$i]);
1565
  $caption = check_plain(_filter_url_trim($match[$i]));
1566
  $match[$i] = check_plain($match[$i]);
1567
  return '<a href="' . $match[$i] . '">' . $caption . '</a>' . $match[$i + 1];
1568
}
1569

    
1570
/**
1571
 * Makes links out of e-mail addresses.
1572
 *
1573
 * Callback for preg_replace_callback() within _filter_url().
1574
 */
1575
function _filter_url_parse_email_links($match) {
1576
  // The $i:th parenthesis in the regexp contains the URL.
1577
  $i = 0;
1578

    
1579
  $match[$i] = decode_entities($match[$i]);
1580
  $caption = check_plain(_filter_url_trim($match[$i]));
1581
  $match[$i] = check_plain($match[$i]);
1582
  return '<a href="mailto:' . $match[$i] . '">' . $caption . '</a>';
1583
}
1584

    
1585
/**
1586
 * Makes links out of domain names starting with "www."
1587
 *
1588
 * Callback for preg_replace_callback() within _filter_url().
1589
 */
1590
function _filter_url_parse_partial_links($match) {
1591
  // The $i:th parenthesis in the regexp contains the URL.
1592
  $i = 1;
1593

    
1594
  $match[$i] = decode_entities($match[$i]);
1595
  $caption = check_plain(_filter_url_trim($match[$i]));
1596
  $match[$i] = check_plain($match[$i]);
1597
  return '<a href="http://' . $match[$i] . '">' . $caption . '</a>' . $match[$i + 1];
1598
}
1599

    
1600
/**
1601
 * Escapes the contents of HTML comments.
1602
 *
1603
 * Callback for preg_replace_callback() within _filter_url().
1604
 *
1605
 * @param $match
1606
 *   An array containing matches to replace from preg_replace_callback(),
1607
 *   whereas $match[1] is expected to contain the content to be filtered.
1608
 * @param $escape
1609
 *   (optional) A Boolean indicating whether to escape (TRUE) or unescape
1610
 *   comments (FALSE). Defaults to NULL, indicating neither. If TRUE, statically
1611
 *   cached $comments are reset.
1612
 */
1613
function _filter_url_escape_comments($match, $escape = NULL) {
1614
  static $mode, $comments = array();
1615

    
1616
  if (isset($escape)) {
1617
    $mode = $escape;
1618
    if ($escape){
1619
      $comments = array();
1620
    }
1621
    return;
1622
  }
1623

    
1624
  // Replace all HTML coments with a '<!-- [hash] -->' placeholder.
1625
  if ($mode) {
1626
    $content = $match[1];
1627
    $hash = md5($content);
1628
    $comments[$hash] = $content;
1629
    return "<!-- $hash -->";
1630
  }
1631
  // Or replace placeholders with actual comment contents.
1632
  else {
1633
    $hash = $match[1];
1634
    $hash = trim($hash);
1635
    $content = $comments[$hash];
1636
    return "<!--$content-->";
1637
  }
1638
}
1639

    
1640
/**
1641
 * Shortens long URLs to http://www.example.com/long/url...
1642
 */
1643
function _filter_url_trim($text, $length = NULL) {
1644
  static $_length;
1645
  if ($length !== NULL) {
1646
    $_length = $length;
1647
  }
1648

    
1649
  // Use +3 for '...' string length.
1650
  if ($_length && strlen($text) > $_length + 3) {
1651
    $text = substr($text, 0, $_length) . '...';
1652
  }
1653

    
1654
  return $text;
1655
}
1656

    
1657
/**
1658
 * Implements callback_filter_tips().
1659
 *
1660
 * Provides help for the URL filter.
1661
 *
1662
 * @see filter_filter_info()
1663
 */
1664
function _filter_url_tips($filter, $format, $long = FALSE) {
1665
  return t('Web page addresses and e-mail addresses turn into links automatically.');
1666
}
1667

    
1668
/**
1669
 * Implements callback_filter_process().
1670
 *
1671
 * Scans the input and makes sure that HTML tags are properly closed.
1672
 */
1673
function _filter_htmlcorrector($text) {
1674
  return filter_dom_serialize(filter_dom_load($text));
1675
}
1676

    
1677
/**
1678
 * Implements callback_filter_process().
1679
 *
1680
 * Converts line breaks into <p> and <br> in an intelligent fashion.
1681
 *
1682
 * Based on: http://photomatt.net/scripts/autop
1683
 */
1684
function _filter_autop($text) {
1685
  // All block level tags
1686
  $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)';
1687

    
1688
  // Split at opening and closing PRE, SCRIPT, STYLE, OBJECT, IFRAME tags
1689
  // and comments. We don't apply any processing to the contents of these tags
1690
  // to avoid messing up code. We look for matched pairs and allow basic
1691
  // nesting. For example:
1692
  // "processed <pre> ignored <script> ignored </script> ignored </pre> processed"
1693
  $chunks = preg_split('@(<!--.*?-->|</?(?:pre|script|style|object|iframe|!--)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
1694
  // Note: PHP ensures the array consists of alternating delimiters and literals
1695
  // and begins and ends with a literal (inserting NULL as required).
1696
  $ignore = FALSE;
1697
  $ignoretag = '';
1698
  $output = '';
1699
  foreach ($chunks as $i => $chunk) {
1700
    if ($i % 2) {
1701
      $comment = (substr($chunk, 0, 4) == '<!--');
1702
      if ($comment) {
1703
        // Nothing to do, this is a comment.
1704
        $output .= $chunk;
1705
        continue;
1706
      }
1707
      // Opening or closing tag?
1708
      $open = ($chunk[1] != '/');
1709
      list($tag) = preg_split('/[ >]/', substr($chunk, 2 - $open), 2);
1710
      if (!$ignore) {
1711
        if ($open) {
1712
          $ignore = TRUE;
1713
          $ignoretag = $tag;
1714
        }
1715
      }
1716
      // Only allow a matching tag to close it.
1717
      elseif (!$open && $ignoretag == $tag) {
1718
        $ignore = FALSE;
1719
        $ignoretag = '';
1720
      }
1721
    }
1722
    elseif (!$ignore) {
1723
      $chunk = preg_replace('|\n*$|', '', $chunk) . "\n\n"; // just to make things a little easier, pad the end
1724
      $chunk = preg_replace('|<br />\s*<br />|', "\n\n", $chunk);
1725
      $chunk = preg_replace('!(<' . $block . '[^>]*>)!', "\n$1", $chunk); // Space things out a little
1726
      $chunk = preg_replace('!(</' . $block . '>)!', "$1\n\n", $chunk); // Space things out a little
1727
      $chunk = preg_replace("/\n\n+/", "\n\n", $chunk); // take care of duplicates
1728
      $chunk = preg_replace('/^\n|\n\s*\n$/', '', $chunk);
1729
      $chunk = '<p>' . preg_replace('/\n\s*\n\n?(.)/', "</p>\n<p>$1", $chunk) . "</p>\n"; // make paragraphs, including one at the end
1730
      $chunk = preg_replace("|<p>(<li.+?)</p>|", "$1", $chunk); // problem with nested lists
1731
      $chunk = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $chunk);
1732
      $chunk = str_replace('</blockquote></p>', '</p></blockquote>', $chunk);
1733
      $chunk = preg_replace('|<p>\s*</p>\n?|', '', $chunk); // under certain strange conditions it could create a P of entirely whitespace
1734
      $chunk = preg_replace('!<p>\s*(</?' . $block . '[^>]*>)!', "$1", $chunk);
1735
      $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*</p>!', "$1", $chunk);
1736
      $chunk = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $chunk); // make line breaks
1737
      $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*<br />!', "$1", $chunk);
1738
      $chunk = preg_replace('!<br />(\s*</?(?:p|li|div|th|pre|td|ul|ol)>)!', '$1', $chunk);
1739
      $chunk = preg_replace('/&([^#])(?![A-Za-z0-9]{1,8};)/', '&amp;$1', $chunk);
1740
    }
1741
    $output .= $chunk;
1742
  }
1743
  return $output;
1744
}
1745

    
1746
/**
1747
 * Implements callback_filter_tips().
1748
 *
1749
 * Provides help for the auto-paragraph filter.
1750
 *
1751
 * @see filter_filter_info()
1752
 */
1753
function _filter_autop_tips($filter, $format, $long = FALSE) {
1754
  if ($long) {
1755
    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.');
1756
  }
1757
  else {
1758
    return t('Lines and paragraphs break automatically.');
1759
  }
1760
}
1761

    
1762
/**
1763
 * Implements callback_filter_process().
1764
 *
1765
 * Escapes all HTML tags, so they will be visible instead of being effective.
1766
 */
1767
function _filter_html_escape($text) {
1768
  return trim(check_plain($text));
1769
}
1770

    
1771
/**
1772
 * Implements callback_filter_tips().
1773
 *
1774
 * Provides help for the HTML escaping filter.
1775
 *
1776
 * @see filter_filter_info()
1777
 */
1778
function _filter_html_escape_tips($filter, $format, $long = FALSE) {
1779
  return t('No HTML tags allowed.');
1780
}
1781

    
1782
/**
1783
 * @} End of "Standard filters".
1784
 */