[ Index ] |
|
Code source de Drupal 5.3 |
1 <?php 2 // $Id: filter.module,v 1.160.2.3 2007/08/08 06:54:10 drumm Exp $ 3 4 /** 5 * @file 6 * Framework for handling filtering of content. 7 */ 8 9 // This is a special format ID which means "use the default format". This value 10 // can be passed to the filter APIs as a format ID: this is equivalent to not 11 // passing an explicit format at all. 12 define('FILTER_FORMAT_DEFAULT', 0); 13 14 define('FILTER_HTML_STRIP', 1); 15 define('FILTER_HTML_ESCAPE', 2); 16 17 /** 18 * Implementation of hook_help(). 19 */ 20 function filter_help($section) { 21 switch ($section) { 22 case 'admin/help#filter': 23 $output = '<p>'. t("The filter module allows administrators to configure text input formats for the site. For example, an administrator may want a filter to strip out malicious HTML from user's comments. Administrators may also want to make URLs linkable even if they are only entered in an unlinked format.") .'</p>'; 24 $output .= '<p>'. t('Users can choose between the available input formats when creating or editing content. Administrators can configure which input formats are available to which user roles, as well as choose a default input format. Administrators can also create new input formats. Each input format can be configured to use a selection of filters.') .'</p>'; 25 $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="@filter">Filter page</a>.', array('@filter' => 'http://drupal.org/handbook/modules/filter/')) .'</p>'; 26 return $output; 27 28 case 'admin/settings/filters': 29 return t(' 30 <p><em>Input formats</em> define a way of processing user-supplied text in Drupal. Every input format has its own settings of which <em>filters</em> to apply. Possible filters include stripping out malicious HTML and making URLs clickable.</p> 31 <p>Users can choose between the available input formats when submitting content.</p> 32 <p>Below you can configure which input formats are available to which roles, as well as choose a default input format (used for imported content, for example).</p> 33 <p>Note that (1) the default format is always available to all roles, and (2) all filter formats can always be used by roles with the "administer filters" permission even if they are not explicitly listed in the Roles column of this table.</p>'); 34 35 case 'admin/settings/filters/'. arg(3): 36 return t(' 37 <p>Every <em>filter</em> performs one particular change on the user input, for example stripping out malicious HTML or making URLs clickable. Choose which filters you want to apply to text in this input format.</p> 38 <p>If you notice some filters are causing conflicts in the output, you can <a href="@rearrange">rearrange them</a>.</p>', array('@rearrange' => url('admin/settings/filters/'. arg(3) .'/order'))); 39 40 case 'admin/settings/filters/'. arg(3) .'/configure': 41 return '<p>'. t('If you cannot find the settings for a certain filter, make sure you have enabled it on the <a href="@url">view tab</a> first.', array('@url' => url('admin/settings/filters/'. arg(3)))) .'</p>'; 42 43 case 'admin/settings/filters/'. arg(3) .'/order': 44 return t(' 45 <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> 46 <p>Filters are executed from top-to-bottom. You can use the weight column to rearrange them: heavier filters "sink" to the bottom.</p>'); 47 } 48 } 49 50 /** 51 * Implementation of hook_menu(). 52 */ 53 function filter_menu($may_cache) { 54 $items = array(); 55 56 if ($may_cache) { 57 $items[] = array('path' => 'admin/settings/filters', 58 'title' => t('Input formats'), 59 'description' => t('Configure how content input by users is filtered, including allowed HTML tags, PHP code tags. Also allows enabling of module-provided filters.'), 60 'callback' => 'drupal_get_form', 61 'callback arguments' => array('filter_admin_overview'), 62 'access' => user_access('administer filters'), 63 ); 64 $items[] = array('path' => 'admin/settings/filters/list', 65 'title' => t('List'), 66 'callback' => 'filter_admin_overview', 67 'type' => MENU_DEFAULT_LOCAL_TASK, 68 'access' => user_access('administer filters'), 69 ); 70 $items[] = array('path' => 'admin/settings/filters/add', 71 'title' => t('Add input format'), 72 'callback' => 'drupal_get_form', 73 'callback arguments' => array('filter_admin_format_form'), 74 'type' => MENU_LOCAL_TASK, 75 'weight' => 1, 76 'access' => user_access('administer filters'), 77 ); 78 $items[] = array('path' => 'admin/settings/filters/delete', 79 'title' => t('Delete input format'), 80 'callback' => 'drupal_get_form', 81 'callback arguments' => array('filter_admin_delete'), 82 'type' => MENU_CALLBACK, 83 'access' => user_access('administer filters'), 84 ); 85 $items[] = array('path' => 'filter/tips', 86 'title' => t('Compose tips'), 87 'callback' => 'filter_tips_long', 88 'access' => TRUE, 89 'type' => MENU_SUGGESTED_ITEM, 90 ); 91 } 92 else { 93 if (arg(0) == 'admin' && arg(1) == 'settings' && arg(2) == 'filters' && is_numeric(arg(3))) { 94 $formats = filter_formats(); 95 96 if (isset($formats[arg(3)])) { 97 $items[] = array('path' => 'admin/settings/filters/'. arg(3), 98 'title' => t("!format input format", array('!format' => $formats[arg(3)]->name)), 99 'callback' => 'drupal_get_form', 100 'callback arguments' => array('filter_admin_format_form', $formats[arg(3)]), 101 'type' => MENU_CALLBACK, 102 'access' => user_access('administer filters'), 103 ); 104 $items[] = array('path' => 'admin/settings/filters/'. arg(3) .'/list', 105 'title' => t('View'), 106 'callback' => 'drupal_get_form', 107 'callback arguments' => array('filter_admin_format_form', $formats[arg(3)]), 108 'type' => MENU_DEFAULT_LOCAL_TASK, 109 'weight' => 0, 110 'access' => user_access('administer filters'), 111 ); 112 $items[] = array('path' => 'admin/settings/filters/'. arg(3) .'/configure', 113 'title' => t('Configure'), 114 'callback' => 'drupal_get_form', 115 'callback arguments' => array('filter_admin_configure'), 116 'type' => MENU_LOCAL_TASK, 117 'weight' => 1, 118 'access' => user_access('administer filters'), 119 ); 120 $items[] = array('path' => 'admin/settings/filters/'. arg(3) .'/order', 121 'title' => t('Rearrange'), 122 'callback' => 'drupal_get_form', 123 'callback arguments' => array('filter_admin_order', 'format' => $formats[arg(3)]), 124 'type' => MENU_LOCAL_TASK, 125 'weight' => 2, 126 'access' => user_access('administer filters'), 127 ); 128 } 129 } 130 } 131 132 return $items; 133 } 134 135 /** 136 * Implementation of hook_perm(). 137 */ 138 function filter_perm() { 139 return array('administer filters'); 140 } 141 142 /** 143 * Implementation of hook_cron(). 144 * 145 * Expire outdated filter cache entries 146 */ 147 function filter_cron() { 148 cache_clear_all(NULL, 'cache_filter'); 149 } 150 151 /** 152 * Implementation of hook_filter_tips(). 153 */ 154 function filter_filter_tips($delta, $format, $long = FALSE) { 155 global $base_url; 156 switch ($delta) { 157 case 0: 158 if (variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_STRIP) { 159 if ($allowed_html = variable_get("allowed_html_$format", '<a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>')) { 160 switch ($long) { 161 case 0: 162 return t('Allowed HTML tags: @tags', array('@tags' => $allowed_html)); 163 case 1: 164 $output = '<p>'. t('Allowed HTML tags: @tags', array('@tags' => $allowed_html)) .'</p>'; 165 if (!variable_get("filter_html_help_$format", 1)) { 166 return $output; 167 } 168 169 $output .= t(' 170 <p>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> 171 <p>For more information see W3C\'s <a href="http://www.w3.org/TR/html/">HTML Specifications</a> or use your favorite search engine to find other sites that explain HTML.</p>'); 172 $tips = array( 173 'a' => array( t('Anchors are used to make links to other pages.'), '<a href="'. $base_url .'">'. variable_get('site_name', 'Drupal') .'</a>'), 174 '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')), 175 '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>'), 176 'strong' => array( t('Strong'), '<strong>'. t('Strong'). '</strong>'), 177 'em' => array( t('Emphasized'), '<em>'. t('Emphasized') .'</em>'), 178 'cite' => array( t('Cited'), '<cite>'. t('Cited') .'</cite>'), 179 'code' => array( t('Coded text used to show programming source code'), '<code>'. t('Coded') .'</code>'), 180 'b' => array( t('Bolded'), '<b>'. t('Bolded') .'</b>'), 181 'u' => array( t('Underlined'), '<u>'. t('Underlined') .'</u>'), 182 'i' => array( t('Italicized'), '<i>'. t('Italicized') .'</i>'), 183 'sup' => array( t('Superscripted'), t('<sup>Super</sup>scripted')), 184 'sub' => array( t('Subscripted'), t('<sub>Sub</sub>scripted')), 185 'pre' => array( t('Preformatted'), '<pre>'. t('Preformatted') .'</pre>'), 186 'abbr' => array( t('Abbreviation'), t('<abbr title="Abbreviation">Abbrev.</abbr>')), 187 'acronym' => array( t('Acronym'), t('<acronym title="Three-Letter Acronym">TLA</acronym>')), 188 'blockquote' => array( t('Block quoted'), '<blockquote>'. t('Block quoted') .'</blockquote>'), 189 'q' => array( t('Quoted inline'), '<q>'. t('Quoted inline') .'</q>'), 190 // Assumes and describes tr, td, th. 191 'table' => array( t('Table'), '<table> <tr><th>'. t('Table header') .'</th></tr> <tr><td>'. t('Table cell') .'</td></tr> </table>'), 192 'tr' => NULL, 'td' => NULL, 'th' => NULL, 193 'del' => array( t('Deleted'), '<del>'. t('Deleted') .'</del>'), 194 'ins' => array( t('Inserted'), '<ins>'. t('Inserted') .'</ins>'), 195 // Assumes and describes li. 196 'ol' => array( t('Ordered list - use the <li> to begin each list item'), '<ol> <li>'. t('First item') .'</li> <li>'. t('Second item') .'</li> </ol>'), 197 'ul' => array( t('Unordered list - use the <li> to begin each list item'), '<ul> <li>'. t('First item') .'</li> <li>'. t('Second item') .'</li> </ul>'), 198 'li' => NULL, 199 // Assumes and describes dt and dd. 200 'dl' => array( t('Definition lists are similar to other HTML lists. <dl> begins the definition list, <dt> begins the definition term and <dd> 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>'), 201 'dt' => NULL, 'dd' => NULL, 202 'h1' => array( t('Header'), '<h1>'. t('Title') .'</h1>'), 203 'h2' => array( t('Header'), '<h2>'. t('Subtitle') .'</h2>'), 204 'h3' => array( t('Header'), '<h3>'. t('Subtitle three') .'</h3>'), 205 'h4' => array( t('Header'), '<h4>'. t('Subtitle four') .'</h4>'), 206 'h5' => array( t('Header'), '<h5>'. t('Subtitle five') .'</h5>'), 207 'h6' => array( t('Header'), '<h6>'. t('Subtitle six') .'</h6>') 208 ); 209 $header = array(t('Tag Description'), t('You Type'), t('You Get')); 210 preg_match_all('/<([a-z0-9]+)[^a-z0-9]/i', $allowed_html, $out); 211 foreach ($out[1] as $tag) { 212 if (array_key_exists($tag, $tips)) { 213 if ($tips[$tag]) { 214 $rows[] = array( 215 array('data' => $tips[$tag][0], 'class' => 'description'), 216 array('data' => '<code>'. check_plain($tips[$tag][1]) .'</code>', 'class' => 'type'), 217 array('data' => $tips[$tag][1], 'class' => 'get') 218 ); 219 } 220 } 221 else { 222 $rows[] = array( 223 array('data' => t('No help provided for tag %tag.', array('%tag' => $tag)), 'class' => 'description', 'colspan' => 3), 224 ); 225 } 226 } 227 $output .= theme('table', $header, $rows); 228 229 $output .= t(' 230 <p>Most unusual characters can be directly entered without any problems.</p> 231 <p>If you do encounter problems, try using HTML character entities. A common example looks like &amp; for an ampersand & character. For a full list of entities see HTML\'s <a href="http://www.w3.org/TR/html4/sgml/entities.html">entities</a> page. Some of the available characters include:</p>'); 232 $entities = array( 233 array( t('Ampersand'), '&'), 234 array( t('Greater than'), '>'), 235 array( t('Less than'), '<'), 236 array( t('Quotation mark'), '"'), 237 ); 238 $header = array(t('Character Description'), t('You Type'), t('You Get')); 239 unset($rows); 240 foreach ($entities as $entity) { 241 $rows[] = array( 242 array('data' => $entity[0], 'class' => 'description'), 243 array('data' => '<code>'. check_plain($entity[1]) .'</code>', 'class' => 'type'), 244 array('data' => $entity[1], 'class' => 'get') 245 ); 246 } 247 $output .= theme('table', $header, $rows); 248 return $output; 249 } 250 } 251 else { 252 return t('No HTML tags allowed'); 253 } 254 } 255 break; 256 257 case 1: 258 switch ($long) { 259 case 0: 260 return t('You may post PHP code. You should include <?php ?> tags.'); 261 case 1: 262 return t(' 263 <h4>Using custom PHP code</h4> 264 <p>If you know how to script in PHP, Drupal gives you the power to embed any script you like. It will be executed when the page is viewed and dynamically embedded into the page. This gives you amazing flexibility and power, but of course with that comes danger and insecurity if you do not write good code. If you are not familiar with PHP, SQL or with the site engine, avoid experimenting with PHP because you can corrupt your database or render your site insecure or even unusable! If you do not plan to do fancy stuff with your content then you are probably better off with straight HTML.</p> 265 <p>Remember that the code within each PHP item must be valid PHP code - including things like correctly terminating statements with a semicolon. It is highly recommended that you develop your code separately using a simple test script on top of a test database before migrating to your production environment.</p> 266 <p>Notes:</p><ul><li>You can use global variables, such as configuration parameters, within the scope of your PHP code but remember that global variables which have been given values in your code will retain these values in the engine afterwards.</li><li>register_globals is now set to <strong>off</strong> by default. If you need form information you need to get it from the "superglobals" $_POST, $_GET, etc.</li><li>You can either use the <code>print</code> or <code>return</code> statement to output the actual content for your item.</li></ul> 267 <p>A basic example:</p> 268 <blockquote><p>You want to have a box with the title "Welcome" that you use to greet your visitors. The content for this box could be created by going:</p> 269 <pre> 270 print t("Welcome visitor, ... welcome message goes here ..."); 271 </pre> 272 <p>If we are however dealing with a registered user, we can customize the message by using:</p> 273 <pre> 274 global $user; 275 if ($user->uid) { 276 print t("Welcome $user->name, ... welcome message goes here ..."); 277 } 278 else { 279 print t("Welcome visitor, ... welcome message goes here ..."); 280 } 281 </pre></blockquote> 282 <p>For more in-depth examples, we recommend that you check the existing Drupal code and use it as a starting point, especially for sidebar boxes.</p>'); 283 } 284 285 case 2: 286 switch ($long) { 287 case 0: 288 return t('Lines and paragraphs break automatically.'); 289 case 1: 290 return t('Lines and paragraphs are automatically recognized. The <br /> line break, <p> paragraph and </p> close paragraph tags are inserted automatically. If paragraphs are not recognized simply add a couple blank lines.'); 291 } 292 293 case 3: 294 return t('Web page addresses and e-mail addresses turn into links automatically.'); 295 } 296 } 297 298 /** 299 * Displays a list of all input formats and which one is the default 300 */ 301 function filter_admin_overview() { 302 303 // Overview of all formats. 304 $formats = filter_formats(); 305 $error = FALSE; 306 307 foreach ($formats as $id => $format) { 308 $roles = array(); 309 foreach (user_roles() as $rid => $name) { 310 // Prepare a roles array with roles that may access the filter 311 if (strstr($format->roles, ",$rid,")) { 312 $roles[] = $name; 313 } 314 } 315 $default = ($id == variable_get('filter_default_format', 1)); 316 $options[$id] = ''; 317 $form[$format->name]['id'] = array('#value' => $id); 318 $form[$format->name]['roles'] = array('#value' => $default ? t('All roles may use default format') : ($roles ? implode(', ',$roles) : t('No roles may use this format'))); 319 $form[$format->name]['configure'] = array('#value' => l(t('configure'), 'admin/settings/filters/'. $id)); 320 $form[$format->name]['delete'] = array('#value' => $default ? '' : l(t('delete'), 'admin/settings/filters/delete/'. $id)); 321 } 322 $form['default'] = array('#type' => 'radios', '#options' => $options, '#default_value' => variable_get('filter_default_format', 1)); 323 $form['submit'] = array('#type' => 'submit', '#value' => t('Set default format')); 324 return $form; 325 } 326 327 function filter_admin_overview_submit($form_id, $form_values) { 328 // Process form submission to set the default format 329 if (is_numeric($form_values['default'])) { 330 drupal_set_message(t('Default format updated.')); 331 variable_set('filter_default_format', $form_values['default']); 332 } 333 } 334 335 function theme_filter_admin_overview($form) { 336 $rows = array(); 337 foreach ($form as $name => $element) { 338 if (isset($element['roles']) && is_array($element['roles'])) { 339 $rows[] = array( 340 drupal_render($form['default'][$element['id']['#value']]), 341 check_plain($name), 342 drupal_render($element['roles']), 343 drupal_render($element['configure']), 344 drupal_render($element['delete']) 345 ); 346 unset($form[$name]); 347 } 348 } 349 $header = array(t('Default'), t('Name'), t('Roles'), array('data' => t('Operations'), 'colspan' => 2)); 350 $output = theme('table', $header, $rows); 351 $output .= drupal_render($form); 352 353 return $output; 354 } 355 356 /** 357 * Menu callback; confirm deletion of a format. 358 */ 359 function filter_admin_delete() { 360 $format = arg(4); 361 $format = db_fetch_object(db_query('SELECT * FROM {filter_formats} WHERE format = %d', $format)); 362 363 if ($format) { 364 if ($format->format != variable_get('filter_default_format', 1)) { 365 $form['format'] = array('#type' => 'hidden', '#value' => $format->format); 366 $form['name'] = array('#type' => 'hidden', '#value' => $format->name); 367 368 return confirm_form($form, t('Are you sure you want to delete the input format %format?', array('%format' => $format->name)), 'admin/settings/filters', t('If you have any content left in this input format, it will be switched to the default input format. This action cannot be undone.'), t('Delete'), t('Cancel')); 369 } 370 else { 371 drupal_set_message(t('The default format cannot be deleted.')); 372 drupal_goto('admin/settings/filters'); 373 } 374 } 375 else { 376 drupal_not_found(); 377 } 378 } 379 380 /** 381 * Process filter delete form submission. 382 */ 383 function filter_admin_delete_submit($form_id, $form_values) { 384 db_query("DELETE FROM {filter_formats} WHERE format = %d", $form_values['format']); 385 db_query("DELETE FROM {filters} WHERE format = %d", $form_values['format']); 386 387 $default = variable_get('filter_default_format', 1); 388 // Replace existing instances of the deleted format with the default format. 389 db_query("UPDATE {node_revisions} SET format = %d WHERE format = %d", $default, $form_values['format']); 390 db_query("UPDATE {comments} SET format = %d WHERE format = %d", $default, $form_values['format']); 391 db_query("UPDATE {boxes} SET format = %d WHERE format = %d", $default, $form_values['format']); 392 393 cache_clear_all($form_values['format'] .':', 'cache_filter', TRUE); 394 drupal_set_message(t('Deleted input format %format.', array('%format' => $form_values['name']))); 395 396 return 'admin/settings/filters'; 397 } 398 399 /** 400 * Generate a filter format form. 401 */ 402 function filter_admin_format_form($format = NULL) { 403 $default = ($format->format == variable_get('filter_default_format', 1)); 404 if ($default) { 405 $help = t('All roles for the default format must be enabled and cannot be changed.'); 406 $form['default_format'] = array('#type' => 'hidden', '#value' => 1); 407 } 408 409 $form['name'] = array('#type' => 'textfield', 410 '#title' => 'Name', 411 '#default_value' => $format->name, 412 '#description' => t('Specify a unique name for this filter format.'), 413 '#required' => TRUE, 414 ); 415 416 // Add a row of checkboxes for form group. 417 $form['roles'] = array('#type' => 'fieldset', 418 '#title' => t('Roles'), 419 '#description' => $default ? $help : t('Choose which roles may use this filter format. Note that roles with the "administer filters" permission can always use all the filter formats.'), 420 '#tree' => TRUE, 421 ); 422 423 foreach (user_roles() as $rid => $name) { 424 $checked = strstr($format->roles, ",$rid,"); 425 $form['roles'][$rid] = array('#type' => 'checkbox', 426 '#title' => $name, 427 '#default_value' => ($default || $checked), 428 ); 429 if ($default) { 430 $form['roles'][$rid]['#disabled'] = TRUE; 431 } 432 } 433 // Table with filters 434 $all = filter_list_all(); 435 $enabled = filter_list_format($format->format); 436 437 $form['filters'] = array('#type' => 'fieldset', 438 '#title' => t('Filters'), 439 '#description' => t('Choose the filters that will be used in this filter format.'), 440 '#tree' => TRUE, 441 ); 442 foreach ($all as $id => $filter) { 443 $form['filters'][$id] = array('#type' => 'checkbox', 444 '#title' => $filter->name, 445 '#default_value' => isset($enabled[$id]), 446 '#description' => module_invoke($filter->module, 'filter', 'description', $filter->delta), 447 ); 448 } 449 if (isset($format)) { 450 $form['format'] = array('#type' => 'hidden', '#value' => $format->format); 451 452 // Composition tips (guidelines) 453 $tips = _filter_tips($format->format, FALSE); 454 $extra = '<p>'. l(t('More information about formatting options'), 'filter/tips') .'</p>'; 455 $tiplist = theme('filter_tips', $tips, FALSE, $extra); 456 if (!$tiplist) { 457 $tiplist = '<p>'. t('No guidelines available.') .'</p>'; 458 } 459 $group = '<p>'. t('These are the guidelines that users will see for posting in this input format. They are automatically generated from the filter settings.') .'</p>'; 460 $group .= $tiplist; 461 $form['tips'] = array('#value' => '<h2>'. t('Formatting guidelines') .'</h2>'. $group); 462 } 463 $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration')); 464 465 return $form; 466 } 467 468 /** 469 * Validate filter format form submissions. 470 */ 471 function filter_admin_format_form_validate($form_id, $form_values) { 472 if (!isset($form_values['format'])) { 473 $name = trim($form_values['name']); 474 $result = db_fetch_object(db_query("SELECT format FROM {filter_formats} WHERE name='%s'", $name)); 475 if ($result) { 476 form_set_error('name', t('Filter format names need to be unique. A format named %name already exists.', array('%name' => $name))); 477 } 478 } 479 } 480 481 /** 482 * Process filter format form submissions. 483 */ 484 function filter_admin_format_form_submit($form_id, $form_values) { 485 $format = isset($form_values['format']) ? $form_values['format'] : NULL; 486 $current = filter_list_format($format); 487 $name = trim($form_values['name']); 488 $cache = TRUE; 489 490 // Add a new filter format. 491 if (!$format) { 492 $new = TRUE; 493 db_query("INSERT INTO {filter_formats} (name) VALUES ('%s')", $name); 494 $format = db_result(db_query("SELECT MAX(format) AS format FROM {filter_formats}")); 495 drupal_set_message(t('Added input format %format.', array('%format' => $name))); 496 } 497 else { 498 drupal_set_message(t('The input format settings have been updated.')); 499 } 500 501 db_query("DELETE FROM {filters} WHERE format = %d", $format); 502 foreach ($form_values['filters'] as $id => $checked) { 503 if ($checked) { 504 list($module, $delta) = explode('/', $id); 505 // Add new filters to the bottom. 506 $weight = isset($current[$id]->weight) ? $current[$id]->weight : 10; 507 db_query("INSERT INTO {filters} (format, module, delta, weight) VALUES (%d, '%s', %d, %d)", $format, $module, $delta, $weight); 508 509 // Check if there are any 'no cache' filters. 510 $cache &= !module_invoke($module, 'filter', 'no cache', $delta); 511 } 512 } 513 514 // We store the roles as a string for ease of use. 515 // We should always set all roles to TRUE when saving a default role. 516 // We use leading and trailing comma's to allow easy substring matching. 517 $roles = array(); 518 if (isset($form_values['roles'])) { 519 foreach ($form_values['roles'] as $id => $checked) { 520 if ($checked) { 521 $roles[] = $id; 522 } 523 } 524 } 525 $roles = ','. implode(',', ($form_values['default_format'] ? array_keys(user_roles()) : $roles)) .','; 526 527 db_query("UPDATE {filter_formats} SET cache = %d, name='%s', roles = '%s' WHERE format = %d", $cache, $name, $roles, $format); 528 529 cache_clear_all($format .':', 'cache_filter', TRUE); 530 531 // If a new filter was added, return to the main list of filters. Otherwise, stay on edit filter page to show new changes. 532 if ($new) { 533 return 'admin/settings/filters/'; 534 } 535 else { 536 return 'admin/settings/filters/'. $format; 537 } 538 } 539 540 /** 541 * Menu callback; display form for ordering filters for a format. 542 */ 543 function filter_admin_order($format = NULL) { 544 // Get list (with forced refresh) 545 $filters = filter_list_format($format->format); 546 547 $form['weights'] = array('#tree' => TRUE); 548 foreach ($filters as $id => $filter) { 549 $form['names'][$id] = array('#value' => $filter->name); 550 $form['weights'][$id] = array('#type' => 'weight', '#default_value' => $filter->weight); 551 } 552 $form['format'] = array('#type' => 'hidden', '#value' => $format->format); 553 $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration')); 554 555 return $form; 556 } 557 558 /** 559 * Theme filter order configuration form. 560 */ 561 function theme_filter_admin_order($form) { 562 $header = array(t('Name'), t('Weight')); 563 $rows = array(); 564 foreach (element_children($form['names']) as $id) { 565 // Don't take form control structures 566 if (is_array($form['names'][$id])) { 567 $rows[] = array(drupal_render($form['names'][$id]), drupal_render($form['weights'][$id])); 568 } 569 } 570 571 $output = theme('table', $header, $rows); 572 $output .= drupal_render($form); 573 574 return $output; 575 } 576 577 /** 578 * Process filter order configuration form submission. 579 */ 580 function filter_admin_order_submit($form_id, $form_values) { 581 foreach ($form_values['weights'] as $id => $weight) { 582 list($module, $delta) = explode('/', $id); 583 db_query("UPDATE {filters} SET weight = %d WHERE format = %d AND module = '%s' AND delta = %d", $weight, $form_values['format'], $module, $delta); 584 } 585 drupal_set_message(t('The filter ordering has been saved.')); 586 587 cache_clear_all($form_values['format'] .':', 'cache_filter', TRUE); 588 } 589 590 /** 591 * Menu callback; display settings defined by filters. 592 */ 593 function filter_admin_configure() { 594 $format = arg(3); 595 596 $list = filter_list_format($format); 597 $form = array(); 598 foreach ($list as $filter) { 599 $form_module = module_invoke($filter->module, 'filter', 'settings', $filter->delta, $format); 600 if (isset($form_module) && is_array($form_module)) { 601 $form = array_merge($form, $form_module); 602 } 603 } 604 605 if (!empty($form)) { 606 $form = system_settings_form($form); 607 } 608 else { 609 $form['error'] = array('#value' => t('No settings are available.')); 610 } 611 612 return $form; 613 } 614 615 /** 616 * Retrieve a list of input formats. 617 */ 618 function filter_formats() { 619 global $user; 620 static $formats; 621 622 // Administrators can always use all input formats. 623 $all = user_access('administer filters'); 624 625 if (!isset($formats)) { 626 $formats = array(); 627 628 $query = 'SELECT * FROM {filter_formats}'; 629 630 // Build query for selecting the format(s) based on the user's roles. 631 $args = array(); 632 if (!$all) { 633 $where = array(); 634 foreach ($user->roles as $rid => $role) { 635 $where[] = "roles LIKE '%%,%d,%%'"; 636 $args[] = $rid; 637 } 638 $query .= ' WHERE '. implode(' OR ', $where) . ' OR format = %d'; 639 $args[] = variable_get('filter_default_format', 1); 640 } 641 642 $result = db_query($query, $args); 643 while ($format = db_fetch_object($result)) { 644 $formats[$format->format] = $format; 645 } 646 } 647 return $formats; 648 } 649 650 /** 651 * Build a list of all filters. 652 */ 653 function filter_list_all() { 654 $filters = array(); 655 656 foreach (module_list() as $module) { 657 $list = module_invoke($module, 'filter', 'list'); 658 if (isset($list) && is_array($list)) { 659 foreach ($list as $delta => $name) { 660 $filters[$module .'/'. $delta] = (object)array('module' => $module, 'delta' => $delta, 'name' => $name); 661 } 662 } 663 } 664 665 uasort($filters, '_filter_list_cmp'); 666 667 return $filters; 668 } 669 670 /** 671 * Helper function for sorting the filter list by filter name. 672 */ 673 function _filter_list_cmp($a, $b) { 674 return strcmp($a->name, $b->name); 675 } 676 677 /** 678 * Resolve a format id, including the default format. 679 */ 680 function filter_resolve_format($format) { 681 return $format == FILTER_FORMAT_DEFAULT ? variable_get('filter_default_format', 1) : $format; 682 } 683 /** 684 * Check if text in a certain input format is allowed to be cached. 685 */ 686 function filter_format_allowcache($format) { 687 static $cache = array(); 688 $format = filter_resolve_format($format); 689 if (!isset($cache[$format])) { 690 $cache[$format] = db_result(db_query('SELECT cache FROM {filter_formats} WHERE format = %d', $format)); 691 } 692 return $cache[$format]; 693 } 694 695 /** 696 * Retrieve a list of filters for a certain format. 697 */ 698 function filter_list_format($format) { 699 static $filters = array(); 700 701 if (!isset($filters[$format])) { 702 $filters[$format] = array(); 703 $result = db_query("SELECT * FROM {filters} WHERE format = %d ORDER BY weight ASC", $format); 704 while ($filter = db_fetch_object($result)) { 705 $list = module_invoke($filter->module, 'filter', 'list'); 706 if (isset($list) && is_array($list) && isset($list[$filter->delta])) { 707 $filter->name = $list[$filter->delta]; 708 $filters[$format][$filter->module .'/'. $filter->delta] = $filter; 709 } 710 } 711 } 712 713 return $filters[$format]; 714 } 715 716 /** 717 * @name Filtering functions 718 * @{ 719 * Modules which need to have content filtered can use these functions to 720 * interact with the filter system. 721 * 722 * For more info, see the hook_filter() documentation. 723 * 724 * Note: because filters can inject JavaScript or execute PHP code, security is 725 * vital here. When a user supplies a $format, you should validate it with 726 * filter_access($format) before accepting/using it. This is normally done in 727 * the validation stage of the node system. You should for example never make a 728 * preview of content in a disallowed format. 729 */ 730 731 /** 732 * Run all the enabled filters on a piece of text. 733 * 734 * @param $text 735 * The text to be filtered. 736 * @param $format 737 * The format of the text to be filtered. Specify FILTER_FORMAT_DEFAULT for 738 * the default format. 739 * @param $check 740 * Whether to check the $format with filter_access() first. Defaults to TRUE. 741 * Note that this will check the permissions of the current user, so you 742 * should specify $check = FALSE when viewing other people's content. When 743 * showing content that is not (yet) stored in the database (eg. upon preview), 744 * set to TRUE so the user's permissions are checked. 745 */ 746 function check_markup($text, $format = FILTER_FORMAT_DEFAULT, $check = TRUE) { 747 // When $check = TRUE, do an access check on $format. 748 if (isset($text) && (!$check || filter_access($format))) { 749 $format = filter_resolve_format($format); 750 751 // Check for a cached version of this piece of text. 752 $id = $format .':'. md5($text); 753 if ($cached = cache_get($id, 'cache_filter')) { 754 return $cached->data; 755 } 756 757 // See if caching is allowed for this format. 758 $cache = filter_format_allowcache($format); 759 760 // Convert all Windows and Mac newlines to a single newline, 761 // so filters only need to deal with one possibility. 762 $text = str_replace(array("\r\n", "\r"), "\n", $text); 763 764 // Get a complete list of filters, ordered properly. 765 $filters = filter_list_format($format); 766 767 // Give filters the chance to escape HTML-like data such as code or formulas. 768 foreach ($filters as $filter) { 769 $text = module_invoke($filter->module, 'filter', 'prepare', $filter->delta, $format, $text); 770 } 771 772 // Perform filtering. 773 foreach ($filters as $filter) { 774 $text = module_invoke($filter->module, 'filter', 'process', $filter->delta, $format, $text); 775 } 776 777 // Store in cache with a minimum expiration time of 1 day. 778 if ($cache) { 779 cache_set($id, 'cache_filter', $text, time() + (60 * 60 * 24)); 780 } 781 } 782 else { 783 $text = t('n/a'); 784 } 785 786 return $text; 787 } 788 789 /** 790 * Generate a selector for choosing a format in a form. 791 * 792 * @param $value 793 * The ID of the format that is currently selected. 794 * @param $weight 795 * The weight of the input format. 796 * @param $parents 797 * Required when defining multiple input formats on a single node or having a different parent than 'format'. 798 * @return 799 * HTML for the form element. 800 */ 801 function filter_form($value = FILTER_FORMAT_DEFAULT, $weight = NULL, $parents = array('format')) { 802 $value = filter_resolve_format($value); 803 $formats = filter_formats(); 804 805 $extra = theme('filter_tips_more_info'); 806 807 if (count($formats) > 1) { 808 $form = array( 809 '#type' => 'fieldset', 810 '#title' => t('Input format'), 811 '#collapsible' => TRUE, 812 '#collapsed' => TRUE, 813 '#weight' => $weight, 814 '#validate' => array('filter_form_validate' => array()), 815 ); 816 // Multiple formats available: display radio buttons with tips. 817 foreach ($formats as $format) { 818 $form[$format->format] = array( 819 '#type' => 'radio', 820 '#title' => $format->name, 821 '#default_value' => $value, 822 '#return_value' => $format->format, 823 '#parents' => $parents, 824 '#description' => theme('filter_tips', _filter_tips($format->format, FALSE)), 825 ); 826 } 827 } 828 else { 829 // Only one format available: use a hidden form item and only show tips. 830 $format = array_shift($formats); 831 $form[$format->format] = array('#type' => 'value', '#value' => $format->format, '#parents' => $parents); 832 $tips = _filter_tips(variable_get('filter_default_format', 1), FALSE); 833 $form['format']['guidelines'] = array( 834 '#title' => t('Formatting guidelines'), 835 '#value' => theme('filter_tips', $tips, FALSE, $extra), 836 ); 837 } 838 $form[] = array('#value' => $extra); 839 return $form; 840 } 841 842 function filter_form_validate($form) { 843 foreach (element_children($form) as $key) { 844 if ($form[$key]['#value'] == $form[$key]['#return_value']) { 845 return; 846 } 847 } 848 form_error($form, t('An illegal choice has been detected. Please contact the site administrator.')); 849 watchdog('form', t('Illegal choice %choice in %name element.', array('%choice' => $form[$key]['#value'], '%name' => empty($form['#title']) ? $form['#parents'][0] : $form['#title'])), WATCHDOG_ERROR); 850 } 851 852 /** 853 * Returns TRUE if the user is allowed to access this format. 854 */ 855 function filter_access($format) { 856 $format = filter_resolve_format($format); 857 if (user_access('administer filters') || ($format == variable_get('filter_default_format', 1))) { 858 return TRUE; 859 } 860 else { 861 $formats = filter_formats(); 862 return isset($formats[$format]); 863 } 864 } 865 /** 866 * @} End of "Filtering functions". 867 */ 868 869 /** 870 * Menu callback; show a page with long filter tips. 871 */ 872 function filter_tips_long() { 873 $format = arg(2); 874 if ($format) { 875 $output = theme('filter_tips', _filter_tips($format, TRUE), TRUE); 876 } 877 else { 878 $output = theme('filter_tips', _filter_tips(-1, TRUE), TRUE); 879 } 880 return $output; 881 } 882 883 /** 884 * Helper function for fetching filter tips. 885 */ 886 function _filter_tips($format, $long = FALSE) { 887 if ($format == -1) { 888 $formats = filter_formats(); 889 } 890 else { 891 $formats = array(db_fetch_object(db_query("SELECT * FROM {filter_formats} WHERE format = %d", $format))); 892 } 893 894 $tips = array(); 895 896 foreach ($formats as $format) { 897 $filters = filter_list_format($format->format); 898 899 $tips[$format->name] = array(); 900 foreach ($filters as $id => $filter) { 901 if ($tip = module_invoke($filter->module, 'filter_tips', $filter->delta, $format->format, $long)) { 902 $tips[$format->name][] = array('tip' => $tip, 'id' => $id); 903 } 904 } 905 } 906 907 return $tips; 908 } 909 910 /** 911 * Format a set of filter tips. 912 * 913 * @ingroup themeable 914 */ 915 function theme_filter_tips($tips, $long = FALSE, $extra = '') { 916 $output = ''; 917 918 $multiple = count($tips) > 1; 919 if ($multiple) { 920 $output = t('input formats') .':'; 921 } 922 923 if (count($tips)) { 924 if ($multiple) { 925 $output .= '<ul>'; 926 } 927 foreach ($tips as $name => $tiplist) { 928 if ($multiple) { 929 $output .= '<li>'; 930 $output .= '<strong>'. $name .'</strong>:<br />'; 931 } 932 933 $tips = ''; 934 foreach ($tiplist as $tip) { 935 $tips .= '<li'. ($long ? ' id="filter-'. str_replace("/", "-", $tip['id']) .'">' : '>') . $tip['tip'] . '</li>'; 936 } 937 938 if ($tips) { 939 $output .= "<ul class=\"tips\">$tips</ul>"; 940 } 941 942 if ($multiple) { 943 $output .= '</li>'; 944 } 945 } 946 if ($multiple) { 947 $output .= '</ul>'; 948 } 949 } 950 951 return $output; 952 } 953 954 /** 955 * Format a link to the more extensive filter tips. 956 * 957 * @ingroup themeable 958 */ 959 960 function theme_filter_tips_more_info() { 961 return '<p>'. l(t('More information about formatting options'), 'filter/tips') .'</p>'; 962 } 963 964 /** 965 * @name Standard filters 966 * @{ 967 * Filters implemented by the filter.module. 968 */ 969 970 /** 971 * Implementation of hook_filter(). Contains a basic set of essential filters. 972 * - HTML filter: 973 * Validates user-supplied HTML, transforming it as necessary. 974 * - PHP evaluator: 975 * Executes PHP code. 976 * - Line break converter: 977 * Converts newlines into paragraph and break tags. 978 */ 979 function filter_filter($op, $delta = 0, $format = -1, $text = '') { 980 switch ($op) { 981 case 'list': 982 return array(0 => t('HTML filter'), 1 => t('PHP evaluator'), 2 => t('Line break converter'), 3 => t('URL filter')); 983 984 case 'no cache': 985 return $delta == 1; // No caching for the PHP evaluator. 986 987 case 'description': 988 switch ($delta) { 989 case 0: 990 return t('Allows you to restrict if users can post HTML and which tags to filter out.'); 991 case 1: 992 return t('Runs a piece of PHP code. The usage of this filter should be restricted to administrators only!'); 993 case 2: 994 return t('Converts line breaks into HTML (i.e. <br> and <p> tags).'); 995 case 3: 996 return t('Turns web and e-mail addresses into clickable links.'); 997 default: 998 return; 999 } 1000 1001 case 'process': 1002 switch ($delta) { 1003 case 0: 1004 return _filter_html($text, $format); 1005 case 1: 1006 return drupal_eval($text); 1007 case 2: 1008 return _filter_autop($text); 1009 case 3: 1010 return _filter_url($text, $format); 1011 default: 1012 return $text; 1013 } 1014 1015 case 'settings': 1016 switch ($delta) { 1017 case 0: 1018 return _filter_html_settings($format); 1019 case 3: 1020 return _filter_url_settings($format); 1021 default: 1022 return; 1023 } 1024 1025 default: 1026 return $text; 1027 } 1028 } 1029 1030 /** 1031 * Settings for the HTML filter. 1032 */ 1033 function _filter_html_settings($format) { 1034 $form['filter_html'] = array( 1035 '#type' => 'fieldset', 1036 '#title' => t('HTML filter'), 1037 '#collapsible' => TRUE, 1038 ); 1039 $form['filter_html']["filter_html_$format"] = array( 1040 '#type' => 'radios', 1041 '#title' => t('Filter HTML tags'), 1042 '#default_value' => variable_get("filter_html_$format", FILTER_HTML_STRIP), 1043 '#options' => array(FILTER_HTML_STRIP => t('Strip disallowed tags'), FILTER_HTML_ESCAPE => t('Escape all tags')), 1044 '#description' => t('How to deal with HTML tags in user-contributed content. If set to "Strip disallowed tags", dangerous tags are removed (see below). If set to "Escape tags", all HTML is escaped and presented as it was typed.'), 1045 ); 1046 $form['filter_html']["allowed_html_$format"] = array( 1047 '#type' => 'textfield', 1048 '#title' => t('Allowed HTML tags'), 1049 '#default_value' => variable_get("allowed_html_$format", '<a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>'), 1050 '#size' => 64, 1051 '#maxlength' => 255, 1052 '#description' => t('If "Strip disallowed tags" is selected, optionally specify tags which should not be stripped. JavaScript event attributes are always stripped.'), 1053 ); 1054 $form['filter_html']["filter_html_help_$format"] = array( 1055 '#type' => 'checkbox', 1056 '#title' => t('Display HTML help'), 1057 '#default_value' => variable_get("filter_html_help_$format", 1), 1058 '#description' => t('If enabled, Drupal will display some basic HTML help in the long filter tips.'), 1059 ); 1060 $form['filter_html']["filter_html_nofollow_$format"] = array( 1061 '#type' => 'checkbox', 1062 '#title' => t('Spam link deterrent'), 1063 '#default_value' => variable_get("filter_html_nofollow_$format", FALSE), 1064 '#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.'), 1065 ); 1066 return $form; 1067 } 1068 1069 /** 1070 * HTML filter. Provides filtering of input into accepted HTML. 1071 */ 1072 function _filter_html($text, $format) { 1073 if (variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_STRIP) { 1074 $allowed_tags = preg_split('/\s+|<|>/', variable_get("allowed_html_$format", '<a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>'), -1, PREG_SPLIT_NO_EMPTY); 1075 $text = filter_xss($text, $allowed_tags); 1076 } 1077 1078 if (variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_ESCAPE) { 1079 // Escape HTML 1080 $text = check_plain($text); 1081 } 1082 1083 if (variable_get("filter_html_nofollow_$format", FALSE)) { 1084 $text = preg_replace('/<a([^>]+)>/i', '<a\\1 rel="nofollow">', $text); 1085 } 1086 1087 return trim($text); 1088 } 1089 1090 /** 1091 * Settings for URL filter. 1092 */ 1093 function _filter_url_settings($format) { 1094 $form['filter_urlfilter'] = array( 1095 '#type' => 'fieldset', 1096 '#title' => t('URL filter'), 1097 '#collapsible' => TRUE, 1098 ); 1099 $form['filter_urlfilter']['filter_url_length_'. $format] = array( 1100 '#type' => 'textfield', 1101 '#title' => t('Maximum link text length'), 1102 '#default_value' => variable_get('filter_url_length_'. $format, 72), 1103 '#maxlength' => 4, 1104 '#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.'), 1105 ); 1106 return $form; 1107 } 1108 1109 /** 1110 * URL filter. Automatically converts text web addresses (URLs, e-mail addresses, 1111 * ftp links, etc.) into hyperlinks. 1112 */ 1113 function _filter_url($text, $format) { 1114 // Pass length to regexp callback 1115 _filter_url_trim(NULL, variable_get('filter_url_length_'. $format, 72)); 1116 1117 $text = ' '. $text .' '; 1118 1119 // Match absolute URLs. 1120 $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); 1121 1122 // Match e-mail addresses. 1123 $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); 1124 1125 // Match www domains/addresses. 1126 $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); 1127 $text = substr($text, 1, -1); 1128 1129 return $text; 1130 } 1131 1132 /** 1133 * Make links out of absolute URLs. 1134 */ 1135 function _filter_url_parse_full_links($match) { 1136 $match[2] = decode_entities($match[2]); 1137 $caption = check_plain(_filter_url_trim($match[2])); 1138 $match[2] = check_url($match[2]); 1139 return $match[1] . '<a href="'. $match[2] .'" title="'. $match[2] .'">'. $caption .'</a>'. $match[5]; 1140 } 1141 1142 /** 1143 * Make links out of domain names starting with "www." 1144 */ 1145 function _filter_url_parse_partial_links($match) { 1146 $match[2] = decode_entities($match[2]); 1147 $caption = check_plain(_filter_url_trim($match[2])); 1148 $match[2] = check_plain($match[2]); 1149 return $match[1] . '<a href="http://'. $match[2] .'" title="'. $match[2] .'">'. $caption .'</a>'. $match[3]; 1150 } 1151 1152 /** 1153 * Shortens long URLs to http://www.example.com/long/url... 1154 */ 1155 function _filter_url_trim($text, $length = NULL) { 1156 static $_length; 1157 if ($length !== NULL) { 1158 $_length = $length; 1159 } 1160 1161 if (strlen($text) > $_length) { 1162 $text = substr($text, 0, $_length) .'...'; 1163 } 1164 1165 return $text; 1166 } 1167 1168 /** 1169 * Convert line breaks into <p> and <br> in an intelligent fashion. 1170 * Based on: http://photomatt.net/scripts/autop 1171 */ 1172 function _filter_autop($text) { 1173 // All block level tags 1174 $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])'; 1175 1176 // Split at <pre>, <script>, <style> and </pre>, </script>, </style> tags. 1177 // We don't apply any processing to the contents of these tags to avoid messing 1178 // up code. We look for matched pairs and allow basic nesting. For example: 1179 // "processed <pre> ignored <script> ignored </script> ignored </pre> processed" 1180 $chunks = preg_split('@(</?(?:pre|script|style)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE); 1181 // Note: PHP ensures the array consists of alternating delimiters and literals 1182 // and begins and ends with a literal (inserting NULL as required). 1183 $ignore = FALSE; 1184 $ignoretag = ''; 1185 $output = ''; 1186 foreach ($chunks as $i => $chunk) { 1187 if ($i % 2) { 1188 // Opening or closing tag? 1189 $open = ($chunk[1] != '/'); 1190 list($tag) = split('[ >]', substr($chunk, 2 - $open), 2); 1191 if (!$ignore) { 1192 if ($open) { 1193 $ignore = TRUE; 1194 $ignoretag = $tag; 1195 } 1196 } 1197 // Only allow a matching tag to close it. 1198 else if (!$open && $ignoretag == $tag) { 1199 $ignore = FALSE; 1200 $ignoretag = ''; 1201 } 1202 } 1203 else if (!$ignore) { 1204 $chunk = preg_replace('|\n*$|', '', $chunk) ."\n\n"; // just to make things a little easier, pad the end 1205 $chunk = preg_replace('|<br />\s*<br />|', "\n\n", $chunk); 1206 $chunk = preg_replace('!(<'. $block .'[^>]*>)!', "\n$1", $chunk); // Space things out a little 1207 $chunk = preg_replace('!(</'. $block .'>)!', "$1\n\n", $chunk); // Space things out a little 1208 $chunk = preg_replace("/\n\n+/", "\n\n", $chunk); // take care of duplicates 1209 $chunk = preg_replace('/\n?(.+?)(?:\n\s*\n|\z)/s', "<p>$1</p>\n", $chunk); // make paragraphs, including one at the end 1210 $chunk = preg_replace('|<p>\s*</p>\n|', '', $chunk); // under certain strange conditions it could create a P of entirely whitespace 1211 $chunk = preg_replace("|<p>(<li.+?)</p>|", "$1", $chunk); // problem with nested lists 1212 $chunk = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $chunk); 1213 $chunk = str_replace('</blockquote></p>', '</p></blockquote>', $chunk); 1214 $chunk = preg_replace('!<p>\s*(</?'. $block .'[^>]*>)!', "$1", $chunk); 1215 $chunk = preg_replace('!(</?'. $block .'[^>]*>)\s*</p>!', "$1", $chunk); 1216 $chunk = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $chunk); // make line breaks 1217 $chunk = preg_replace('!(</?'. $block .'[^>]*>)\s*<br />!', "$1", $chunk); 1218 $chunk = preg_replace('!<br />(\s*</?(?:p|li|div|th|pre|td|ul|ol)>)!', '$1', $chunk); 1219 $chunk = preg_replace('/&([^#])(?![A-Za-z0-9]{1,8};)/', '&$1', $chunk); 1220 } 1221 $output .= $chunk; 1222 } 1223 return $output; 1224 } 1225 1226 /** 1227 * Very permissive XSS/HTML filter for admin-only use. 1228 * 1229 * Use only for fields where it is impractical to use the 1230 * whole filter system, but where some (mainly inline) mark-up 1231 * is desired (so check_plain() is not acceptable). 1232 * 1233 * Allows all tags that can be used inside an HTML body, save 1234 * for scripts and styles. 1235 */ 1236 function filter_xss_admin($string) { 1237 return filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'object', 'ol', 'p', 'param', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var')); 1238 } 1239 1240 /** 1241 * Filters XSS. Based on kses by Ulf Harnhammar, see 1242 * http://sourceforge.net/projects/kses 1243 * 1244 * For examples of various XSS attacks, see: 1245 * http://ha.ckers.org/xss.html 1246 * 1247 * This code does four things: 1248 * - Removes characters and constructs that can trick browsers 1249 * - Makes sure all HTML entities are well-formed 1250 * - Makes sure all HTML tags and attributes are well-formed 1251 * - Makes sure no HTML tags contain URLs with a disallowed protocol (e.g. javascript:) 1252 * 1253 * @param $string 1254 * The string with raw HTML in it. It will be stripped of everything that can cause 1255 * an XSS attack. 1256 * @param $allowed_tags 1257 * An array of allowed tags. 1258 * @param $format 1259 * The format to use. 1260 */ 1261 function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) { 1262 // Store the input format 1263 _filter_xss_split($allowed_tags, TRUE); 1264 // Remove NUL characters (ignored by some browsers) 1265 $string = str_replace(chr(0), '', $string); 1266 // Remove Netscape 4 JS entities 1267 $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string); 1268 1269 // Defuse all HTML entities 1270 $string = str_replace('&', '&', $string); 1271 // Change back only well-formed entities in our whitelist 1272 // Named entities 1273 $string = preg_replace('/&([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string); 1274 // Decimal numeric entities 1275 $string = preg_replace('/&#([0-9]+;)/', '&#\1', $string); 1276 // Hexadecimal numeric entities 1277 $string = preg_replace('/&#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string); 1278 1279 return preg_replace_callback('% 1280 ( 1281 <(?=[^a-zA-Z!/]) # a lone < 1282 | # or 1283 <[^>]*.(>|$) # a string that starts with a <, up until the > or the end of the string 1284 | # or 1285 > # just a > 1286 )%x', '_filter_xss_split', $string); 1287 } 1288 1289 /** 1290 * Processes an HTML tag. 1291 * 1292 * @param @m 1293 * An array with various meaning depending on the value of $store. 1294 * If $store is TRUE then the array contains the allowed tags. 1295 * If $store is FALSE then the array has one element, the HTML tag to process. 1296 * @param $store 1297 * Whether to store $m. 1298 * @return 1299 * If the element isn't allowed, an empty string. Otherwise, the cleaned up 1300 * version of the HTML element. 1301 */ 1302 function _filter_xss_split($m, $store = FALSE) { 1303 static $allowed_html; 1304 1305 if ($store) { 1306 $allowed_html = array_flip($m); 1307 return; 1308 } 1309 1310 $string = $m[1]; 1311 1312 if (substr($string, 0, 1) != '<') { 1313 // We matched a lone ">" character 1314 return '>'; 1315 } 1316 else if (strlen($string) == 1) { 1317 // We matched a lone "<" character 1318 return '<'; 1319 } 1320 1321 if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?$%', $string, $matches)) { 1322 // Seriously malformed 1323 return ''; 1324 } 1325 1326 $slash = trim($matches[1]); 1327 $elem = &$matches[2]; 1328 $attrlist = &$matches[3]; 1329 1330 if (!isset($allowed_html[strtolower($elem)])) { 1331 // Disallowed HTML element 1332 return ''; 1333 } 1334 1335 if ($slash != '') { 1336 return "</$elem>"; 1337 } 1338 1339 // Is there a closing XHTML slash at the end of the attributes? 1340 // In PHP 5.1.0+ we could count the changes, currently we need a separate match 1341 $xhtml_slash = preg_match('%\s?/\s*$%', $attrlist) ? ' /' : ''; 1342 $attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist); 1343 1344 // Clean up attributes 1345 $attr2 = implode(' ', _filter_xss_attributes($attrlist)); 1346 $attr2 = preg_replace('/[<>]/', '', $attr2); 1347 $attr2 = strlen($attr2) ? ' '. $attr2 : ''; 1348 1349 return "<$elem$attr2$xhtml_slash>"; 1350 } 1351 1352 /** 1353 * Processes a string of HTML attributes. 1354 * 1355 * @return 1356 * Cleaned up version of the HTML attributes. 1357 */ 1358 function _filter_xss_attributes($attr) { 1359 $attrarr = array(); 1360 $mode = 0; 1361 $attrname = ''; 1362 1363 while (strlen($attr) != 0) { 1364 // Was the last operation successful? 1365 $working = 0; 1366 1367 switch ($mode) { 1368 case 0: 1369 // Attribute name, href for instance 1370 if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) { 1371 $attrname = strtolower($match[1]); 1372 $skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on'); 1373 $working = $mode = 1; 1374 $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr); 1375 } 1376 1377 break; 1378 1379 case 1: 1380 // Equals sign or valueless ("selected") 1381 if (preg_match('/^\s*=\s*/', $attr)) { 1382 $working = 1; $mode = 2; 1383 $attr = preg_replace('/^\s*=\s*/', '', $attr); 1384 break; 1385 } 1386 1387 if (preg_match('/^\s+/', $attr)) { 1388 $working = 1; $mode = 0; 1389 if (!$skip) { 1390 $attrarr[] = $attrname; 1391 } 1392 $attr = preg_replace('/^\s+/', '', $attr); 1393 } 1394 1395 break; 1396 1397 case 2: 1398 // Attribute value, a URL after href= for instance 1399 if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) { 1400 $thisval = filter_xss_bad_protocol($match[1]); 1401 1402 if (!$skip) { 1403 $attrarr[] = "$attrname=\"$thisval\""; 1404 } 1405 $working = 1; 1406 $mode = 0; 1407 $attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr); 1408 break; 1409 } 1410 1411 if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match)) { 1412 $thisval = filter_xss_bad_protocol($match[1]); 1413 1414 if (!$skip) { 1415 $attrarr[] = "$attrname='$thisval'";; 1416 } 1417 $working = 1; $mode = 0; 1418 $attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr); 1419 break; 1420 } 1421 1422 if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match)) { 1423 $thisval = filter_xss_bad_protocol($match[1]); 1424 1425 if (!$skip) { 1426 $attrarr[] = "$attrname=\"$thisval\""; 1427 } 1428 $working = 1; $mode = 0; 1429 $attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr); 1430 } 1431 1432 break; 1433 } 1434 1435 if ($working == 0) { 1436 // not well formed, remove and try again 1437 $attr = preg_replace('/ 1438 ^ 1439 ( 1440 "[^"]*("|$) # - a string that starts with a double quote, up until the next double quote or the end of the string 1441 | # or 1442 \'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string 1443 | # or 1444 \S # - a non-whitespace character 1445 )* # any number of the above three 1446 \s* # any number of whitespaces 1447 /x', '', $attr); 1448 $mode = 0; 1449 } 1450 } 1451 1452 // the attribute list ends with a valueless attribute like "selected" 1453 if ($mode == 1) { 1454 $attrarr[] = $attrname; 1455 } 1456 return $attrarr; 1457 } 1458 1459 /** 1460 * Processes an HTML attribute value and ensures it does not contain an URL 1461 * with a disallowed protocol (e.g. javascript:) 1462 * 1463 * @param $string 1464 * The string with the attribute value. 1465 * @param $decode 1466 * Whether to decode entities in the $string. Set to FALSE if the $string 1467 * is in plain text, TRUE otherwise. Defaults to TRUE. 1468 * @return 1469 * Cleaned up and HTML-escaped version of $string. 1470 */ 1471 function filter_xss_bad_protocol($string, $decode = TRUE) { 1472 static $allowed_protocols; 1473 if (!isset($allowed_protocols)) { 1474 $allowed_protocols = array_flip(variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal'))); 1475 } 1476 1477 // Get the plain text representation of the attribute value (i.e. its meaning). 1478 if ($decode) { 1479 $string = decode_entities($string); 1480 } 1481 1482 // Iteratively remove any invalid protocol found. 1483 1484 do { 1485 $before = $string; 1486 $colonpos = strpos($string, ':'); 1487 if ($colonpos > 0) { 1488 // We found a colon, possibly a protocol. Verify. 1489 $protocol = substr($string, 0, $colonpos); 1490 // If a colon is preceded by a slash, question mark or hash, it cannot 1491 // possibly be part of the URL scheme. This must be a relative URL, 1492 // which inherits the (safe) protocol of the base document. 1493 if (preg_match('![/?#]!', $protocol)) { 1494 break; 1495 } 1496 // Per RFC2616, section 3.2.3 (URI Comparison) scheme comparison must be case-insensitive. 1497 // Check if this is a disallowed protocol. 1498 if (!isset($allowed_protocols[strtolower($protocol)])) { 1499 $string = substr($string, $colonpos + 1); 1500 } 1501 } 1502 } while ($before != $string); 1503 return check_plain($string); 1504 } 1505 1506 /** 1507 * @} End of "Standard filters". 1508 */ 1509
titre
Description
Corps
titre
Description
Corps
titre
Description
Corps
titre
Corps
Généré le : Fri Nov 30 16:20:15 2007 | par Balluche grâce à PHPXref 0.7 |
![]() |