summaryrefslogtreecommitdiff
path: root/modules/filter/filter.module
blob: a4a59b5f277e01729fd2c7a43ea54f604d87a7cd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
<?php
// $Id$

define('FILTER_HTML_DONOTHING', 0);
define('FILTER_HTML_STRIP', 1);
define('FILTER_HTML_ESCAPE', 2);

define('FILTER_STYLE_ALLOW', 0);
define('FILTER_STYLE_STRIP', 1);

/**
 * Implementation of hook_help().
 */
function filter_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      return t('Framework for handling filtering of content.');
    case 'admin/filters':
      return t("
<p>Filters fit between the raw text in a node and the HTML output. They allow you to replace text selectively. Uses include automatic conversion of emoticons into graphics and filtering HTML content from users' submissions.</p>
<p>If you notice some filters are causing conflicts in the output, you can <a href=\"%url\">rearrange them</a>.</p>", array('%url' => url('admin/filters/order')));
    case 'admin/filters/order':
      return t("
<p>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 in a clickable link. When this happens, you will need to rearrange the order in which filters get executed.</p>
<p>Filters are executed from top-to-bottom. You can use the weight column to rearrange them: heavier filters 'sink' to the bottom. Standard HTML filtering is always run first.</p>");
    case 'filter#long-tip':
    case 'filter#short-tip':
      switch (variable_get('filter_html', FILTER_HTML_DONOTHING)) {
        case 0:
          return t('All HTML tags allowed');
          break;
        case 1:
          if ($allowed_html = variable_get('allowed_html', '<a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul>')) {
            return t('Allowed HTML tags') .': '. htmlspecialchars($allowed_html);
          } else {
            return t('No HTML tags allowed');
          }
          break;
        case 2:
          return t('No HTML tags allowed');
          break;
      }
      break;
  }
}

/**
 * Implementation of hook_menu().
 */
function filter_menu() {
  $items = array();
  $items[] = array('path' => 'admin/filters', 'title' => t('filters'),
    'callback' => 'filter_admin_settings',
    'access' => user_access('administer site configuration'));
  $items[] = array('path' => 'admin/filters/order', 'title' => t('order filters'),
    'callback' => 'filter_admin_order',
    'access' => user_access('administer site configuration'),
    'type' => MENU_LOCAL_TASK);
  $items[] = array('path' => 'filter/tips', 'title' => t('compose tips'),
    'callback' => 'filter_tips_long', 'access' => TRUE,
    'type' => MENU_SUGGESTED_ITEM);
  return $items;
}

/**
 * Menu callback; allows administrators to change the filter ordering.
 */
function filter_admin_order() {
  $edit = $_POST['edit'];
  $op = $_POST['op'];
  if ($op == t('Save configuration')) {
    foreach ($edit as $module => $filter) {
      db_query("UPDATE {filters} SET weight = %d WHERE module = '%s'", $filter['weight'], $module);
    }
  }

  // Get list (with forced refresh)
  filter_refresh();
  $filters = filter_list();

  $header = array(t('name'), t('weight'));
  $rows = array();

  // Standard HTML filters are always run first, we add a dummy row to indicate this
  $rows[] = array(t('HTML filtering'), array('data' => t('locked')));

  foreach ($filters as $module => $filter) {
    $name = module_invoke($module, 'filter', 'name');
    $rows[] = array($name, array('data' => form_weight(NULL, $module .'][weight', $filter['weight'])));
  }

  $form  = theme('table', $header, $rows);
  $form .= form_submit(t('Save configuration'));
  $output = form($form);

  print theme('page', $output);
}

/**
 * Menu callback; displays settings defined by filters.
 */
function filter_admin_settings() {
  system_settings_save();

  filter_refresh();

  $form  = filter_default_settings();
  $form .= implode("\n", module_invoke_all('filter', 'settings'));
  $output = system_settings_form($form);

  print theme('page', $output);
}

/**
 * Search through all modules for the filters they implement.
 */
function filter_refresh() {
  $modules = module_list();
  $filters = filter_list();

  // Update list in database
  db_query('DELETE FROM {filters}');
  foreach ($modules as $module) {
    if (module_hook($module, 'filter')) {
      $weight = $filters[$module]['weight'];

      db_query("INSERT INTO {filters} (module, weight) VALUES ('%s', %d)", $module, $weight);
    }
  }

  filter_list(1);
}

/**
 * Retrieve a list of all filters from the database.
 */
function filter_list($force = 0) {
  static $filters;

  if (!is_array($filters) || $force) {
    $filters = array();
    $result = db_query('SELECT * FROM {filters} ORDER BY weight ASC');
    while ($filter = db_fetch_array($result)) {
      // Fail-safe in case a module was deleted/changed without disabling it
      if (module_hook($filter['module'], 'filter')) {
        $filters[$filter['module']] = $filter;
      }
    }
  }

  return $filters;
}

/**
 * Run all the enabled filters on a piece of text.
 */
function check_output($text) {
  if (isset($text)) {

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

    // Get complete list of filters ordered properly
    $filters = filter_list();

    // Give filters the chance to escape HTML-like data such as code or formulas.
    // From this point on, the input can be treated as HTML.
    if (variable_get('filter_html', FILTER_HTML_DONOTHING) != FILTER_HTML_ESCAPE) {
      foreach ($filters as $module => $filter) {
        $text = module_invoke($module, 'filter', 'prepare', $text);
      }
    }

    // HTML handling is done before all regular filtering activities.
    $text = filter_default($text);

    // Regular filtering.
    foreach ($filters as $module => $filter) {
      $text = module_invoke($module, 'filter', 'process', $text);
    }

    // If only inline elements are used and no block level elements, we
    // replace all newlines with HTML line breaks.
    if (strip_tags($text, '<a><br><span><bdo><map><object><img><tt><i><b><u><big><small><em><strong><dfn><code><q><samp><kbd><var><cite><abbr><acronym><sub><sup><input><select><textarea><label><button><ins><del><script>') == $text) {
      $text = nl2br($text);
    }
  }
  else {
    $text = message_na();
  }

  return $text;
}

/**
 * Perform the default filters, preventing malicious HTML from being displayed.
 */
function filter_default($text) {
  if (variable_get('filter_html', FILTER_HTML_DONOTHING) == FILTER_HTML_STRIP) {
    // Allow users to enter HTML, but filter it
    $text = strip_tags($text, variable_get('allowed_html', ''));
    if (variable_get('filter_style', FILTER_STYLE_STRIP)) {
      $text = preg_replace('/\Wstyle\s*=[^>]+?>/i', '>', $text);
    }
    $text = preg_replace('/\Won[a-z]+\s*=[^>]+?>/i', '>', $text);
  }

  if (variable_get('filter_html', FILTER_HTML_DONOTHING) == FILTER_HTML_ESCAPE) {
    // Escape HTML
    $text = htmlspecialchars($text);
  }

  return trim($text);
}

/**
 * Settings for the filter system's built-in HTML handling.
 */
function filter_default_settings() {
  $group = form_radios(t('Filter HTML tags'), 'filter_html', variable_get('filter_html', FILTER_HTML_DONOTHING), array(FILTER_HTML_DONOTHING => t('Do not filter'), FILTER_HTML_STRIP => t('Strip tags'), FILTER_HTML_ESCAPE => t('Escape tags')), t('How to deal with HTML and PHP tags in user-contributed content. If set to "Strip tags", dangerous tags are removed (see below).  If set to "Escape tags", all HTML is escaped and presented as it was typed.'));
  $group .= form_textfield(t('Allowed HTML tags'), 'allowed_html', variable_get('allowed_html', '<a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul>'), 64, 255, t('If "Strip tags" is selected, optionally specify tags which should not be stripped.  "ON*" attributes are always stripped.'));
  $group .= form_radios(t('HTML style attributes'), 'filter_style', variable_get('filter_style', FILTER_STYLE_STRIP), array(FILTER_STYLE_ALLOW => t('Allowed'), FILTER_STYLE_STRIP => t('Removed')), t('If "Strip tags" is selected, you can choose whether "STYLE" attributes are allowed or removed from input.'));
  $output .= form_group(t('HTML filtering'), $group);

  return $output;
}

/**
 * Implementation of hook_filter(). Handles URL upgrades from Drupal 4.1.
 */
function filter_filter($op, $text = '') {
  switch ($op) {
    case 'name':
      return t('Legacy filtering');
    case 'process':
      if (variable_get('rewrite_old_urls', 0)) {
        $text = filter_old_urls($text);
      }
      return $text;
    case 'settings':
      $group   = form_radios(t('Rewrite old URLs'), 'rewrite_old_urls', variable_get('rewrite_old_urls', 0), array(t('Disabled'), t('Enabled')), t('The introduction of "clean URLs" in Drupal 4.2.0 breaks internal URLs that date back from Drupal 4.1.0 and before.  If enabled, this filter will attempt to rewrite the old style URLs to avoid broken links.  If <code>mod_rewrite</code> is available on your system, use the rewrite rules in Drupal\'s <code>.htaccess</code> file instead as these will also correct external referrers.'));
      $output .= form_group(t('Legacy filtering'), $group);
      return $output;
    default:
      return $text;
  }
}

/**
 * Rewrite legacy URLs.
 *
 * This is a *temporary* filter to rewrite old-style URLs to new-style
 * URLs (clean URLs).  Currently, URLs are being rewritten dynamically
 * (ie. "on output"), however when these rewrite rules have been tested
 * enough, we will use them to permanently rewrite the links in node
 * and comment bodies.
 */
function filter_old_urls($text) {
  global $base_url;

  $end = substr($base_url, 12);

  if (variable_get('clean_url', '0') == '0') {
    // Relative URLs:

    // rewrite 'node.php?id=<number>[&cid=<number>]' style URLs:
    $text = eregi_replace("\"(node)\.php\?id=([[:digit:]]+)(&cid=)?([[:digit:]]*)", "\"?q=\\1/view/\\2/\\4", $text);

    // rewrite 'module.php?mod=<name>{&<op>=<value>}' style URLs:
    $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "\"?q=\\2/\\4/\\6" , $text);
    $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "\"?q=\\2/\\4", $text);
    $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))", "\"?q=\\2", $text);

    // Absolute URLs:

    // rewrite 'node.php?id=<number>[&cid=<number>]' style URLs:
    $text = eregi_replace("$end/(node)\.php\?id=([[:digit:]]+)(&cid=)?([[:digit:]]*)", "$end/?q=\\1/view/\\2/\\4", $text);

    // rewrite 'module.php?mod=<name>{&<op>=<value>}' style URLs:
    $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "$end/?q=\\2/\\4/\\6" , $text);
    $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "$end/?q=\\2/\\4", $text);
    $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))", "\"$end/?q=\\2", $text);
  }
  else {
    // Relative URLs:

    // rewrite 'node.php?id=<number>[&cid=<number>]' style URLs:
    $text = eregi_replace("\"(node)\.php\?id=([[:digit:]]+)(&cid=)?([[:digit:]]*)", "\"\\1/view/\\2/\\4", $text);

    // rewrite 'module.php?mod=<name>{&<op>=<value>}' style URLs:
    $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "\"\\2/\\4/\\6", $text);
    $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "\"\\2/\\4", $text);
    $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))", "\"\\2", $text);

    // Absolute URLs:

    // rewrite 'node.php?id=<number>[&cid=<number>]' style URLs:
    $text = eregi_replace("$end/(node)\.php\?id=([[:digit:]]+)(&cid=)?([[:digit:]]*)", "$end/\\1/view/\\2/\\4", $text);

    // rewrite 'module.php?mod=<name>{&<op>=<value>}' style URLs:
    $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "$end/\\2/\\4/\\6", $text);
    $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "$end/\\2/\\4", $text);
    $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))", "$end/\\2", $text);
  }

  return $text;
}

/**
 * Fetch full filter help texts defined by modules.
 */
function filter_tips_long() {
  $tiplist = '';
  foreach (module_list() as $name) {
    if ($tip = module_invoke($name, 'help', 'filter#long-tip')) {
      $tiplist .= "<li id=\"filter-$name\">$tip</li>\n";
    }
  }
  $output = "<ul class=\"filter-tips-long\">\n$tiplist\n</ul>\n";
  print theme('page', $output, t('Compose Tips'));
}

/**
 * Fetch abbreviated filter help texts defined by modules.
 */
function filter_tips_short() {
  $tiplist = '';
  foreach (module_list() as $name) {
    if ($tip = module_invoke($name, 'help', 'filter#short-tip')) {
      $tiplist .= "<li>$tip</li>\n";
    }
  }
  $tiplist .= '<li class="more-tips">' . l(t('More information on formatting options'), 'filter/tips') . '</li>';
  return "<ul class=\"filter-tips-short\">\n$tiplist\n</ul>\n";
}

?>