Você está na página 1de 31

<?

php
// $Id: filter.module,v 1.362 2011/01/14 08:33:46 webchick Exp $
/**
* @file
* Framework for handling filtering of content.
*/
/**
* Implements hook_help().
*/
function filter_help($path, $arg) {
switch ($path) {
case 'admin/help#filter':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Filter module allows administrators to configure
text formats. A text format defines the HTML tags, codes, and other input allow
ed in content and comments, and is a key feature in guarding against potentially
damaging input from malicious users. For more information, see the online handb
ook entry for <a href="@filter">Filter module</a>.', array('@filter' => 'http://
drupal.org/handbook/modules/filter/')) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Configuring text formats') . '</dt>';
$output .= '<dd>' . t('Configure text formats on the <a href="@formats">Te
xt formats page</a>. <strong>Improper text format configuration is a security ri
sk</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, sin
ce certain HTML tags can allow embedding malicious links or scripts in text. Mor
e trusted registered users may be granted permission to use less restrictive tex
t formats in order to create rich content.', array('@formats' => url('admin/conf
ig/content/formats'))) . '</dd>';
$output .= '<dt>' . t('Applying filters to text') . '</dt>';
$output .= '<dd>' . t('Each text format uses filters to manipulate text, a
nd most formats apply several different filters to text in a specific order. Eac
h filter is designed for a specific purpose, and generally either adds, removes,
or transforms elements within user-entered text before it is displayed. A filte
r does not change the actual content, but instead, modifies it temporarily befor
e it is displayed. One filter may remove unapproved HTML tags, while another aut
omatically adds HTML to make URLs display as clickable links.') . '</dd>';
$output .= '<dt>' . t('Defining text formats') . '</dt>';
$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 ins
tallation profile when you install Drupal, and more can be created by an adminis
trator on the <a href="@text-formats">Text formats page</a>.', array('@text-form
ats' => url('admin/config/content/formats'))) . '</dd>';
$output .= '<dt>' . t('Choosing a text format') . '</dt>';
$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 w
hen creating or editing multi-line content. Administrators can define the text f
ormats available to each user role, and control the order of formats listed in t
he <em>Text format</em> fieldset on the <a href="@text-formats">Text formats pag
e</a>.', array('@text-formats' => url('admin/config/content/formats'))) . '</dd>
';
$output .= '</dl>';
return $output;
case 'admin/config/content/formats':
$output = '<p>' . t('Text formats define the HTML tags, code, and other fo

rmatting that can be used when entering text. <strong>Improper text format confi
guration is a security risk</strong>. Learn more on the <a href="@filterhelp">Fi
lter module help page</a>.', array('@filterhelp' => url('admin/help/filter'))) .
'</p>';
$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>';
return $output;
case 'admin/config/content/formats/%':
$output = '<p>' . t('A text format contains filters that change the user i
nput, 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 ma
y prevent another filter from doing its job. For example, when URLs are converte
d 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>';
return $output;
}
}
/**
* Implements hook_theme().
*/
function filter_theme() {
return array(
'filter_admin_overview' => array(
'render element' => 'form',
'file' => 'filter.admin.inc',
),
'filter_admin_format_filter_order' => array(
'render element' => 'element',
'file' => 'filter.admin.inc',
),
'filter_tips' => array(
'variables' => array('tips' => NULL, 'long' => FALSE),
'file' => 'filter.pages.inc',
),
'text_format_wrapper' => array(
'render element' => 'element',
),
'filter_tips_more_info' => array(
'variables' => array(),
),
'filter_guidelines' => array(
'variables' => array('format' => NULL),
),
);
}
/**
* Implements hook_element_info().
*
* @see filter_process_format()
*/
function filter_element_info() {
$type['text_format'] = array(
'#process' => array('filter_process_format'),
'#base_type' => 'textarea',
'#theme_wrappers' => array('text_format_wrapper'),
);

return $type;
}
/**
* Implements hook_menu().
*/
function filter_menu() {
$items['filter/tips'] = array(
'title' => 'Compose tips',
'page callback' => 'filter_tips_long',
'access callback' => TRUE,
'type' => MENU_SUGGESTED_ITEM,
'file' => 'filter.pages.inc',
);
$items['admin/config/content/formats'] = array(
'title' => 'Text formats',
'description' => 'Configure how content input by users is filtered, includin
g allowed HTML tags. Also allows enabling of module-provided filters.',
'page callback' => 'drupal_get_form',
'page arguments' => array('filter_admin_overview'),
'access arguments' => array('administer filters'),
'file' => 'filter.admin.inc',
);
$items['admin/config/content/formats/list'] = array(
'title' => 'List',
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['admin/config/content/formats/add'] = array(
'title' => 'Add text format',
'page callback' => 'filter_admin_format_page',
'access arguments' => array('administer filters'),
'type' => MENU_LOCAL_ACTION,
'weight' => 1,
'file' => 'filter.admin.inc',
);
$items['admin/config/content/formats/%filter_format'] = array(
'title callback' => 'filter_admin_format_title',
'title arguments' => array(4),
'page callback' => 'filter_admin_format_page',
'page arguments' => array(4),
'access arguments' => array('administer filters'),
'file' => 'filter.admin.inc',
);
$items['admin/config/content/formats/%filter_format/disable'] = array(
'title' => 'Disable text format',
'page callback' => 'drupal_get_form',
'page arguments' => array('filter_admin_disable', 4),
'access callback' => '_filter_disable_format_access',
'access arguments' => array(4),
'file' => 'filter.admin.inc',
);
return $items;
}
/**
* Access callback for deleting text formats.
*
* @param $format
* A text format object.
* @return

* TRUE if the text format can be disabled by the current user, FALSE
* otherwise.
*/
function _filter_disable_format_access($format) {
// The fallback format can never be disabled.
return user_access('administer filters') && ($format->format != filter_fallbac
k_format());
}
/**
* Load a text format object from the database.
*
* @param $format_id
* The format ID.
*
* @return
* A fully-populated text format object, if the requested format exists and
* is enabled. If the format does not exist, or exists in the database but
* has been marked as disabled, FALSE is returned.
*
* @see filter_format_exists()
*/
function filter_format_load($format_id) {
$formats = filter_formats();
return isset($formats[$format_id]) ? $formats[$format_id] : FALSE;
}
/**
* Save a text format object to the database.
*
* @param $format
* A format object using the properties:
* - 'format': A machine-readable name representing the ID of the text format
*
to save. If this corresponds to an existing text format, that format
*
will be updated; otherwise, a new format will be created.
* - 'name': The title of the text format.
* - 'status': (optional) An integer indicating whether the text format is
*
enabled (1) or not (0). Defaults to 1.
* - 'weight': (optional) The weight of the text format, which controls its
*
placement in text format lists. If omitted, the weight is set to 0.
* - 'filters': (optional) An associative, multi-dimensional array of filters
*
assigned to the text format, keyed by the name of each filter and using
*
the properties:
*
- 'weight': (optional) The weight of the filter in the text format. If
*
omitted, either the currently stored weight is retained (if there is
*
one), or the filter is assigned a weight of 10, which will usually
*
put it at the bottom of the list.
*
- 'status': (optional) A boolean indicating whether the filter is
*
enabled in the text format. If omitted, the filter will be disabled.
*
- 'settings': (optional) An array of configured settings for the filter.
*
See hook_filter_info() for details.
*/
function filter_format_save($format) {
$format->name = trim($format->name);
$format->cache = _filter_format_is_cacheable($format);
if (!isset($format->status)) {
$format->status = 1;
}
if (!isset($format->weight)) {
$format->weight = 0;

}
// Insert or update the text format.
$return = db_merge('filter_format')
->key(array('format' => $format->format))
->fields(array(
'name' => $format->name,
'cache' => (int) $format->cache,
'status' => (int) $format->status,
'weight' => (int) $format->weight,
))
->execute();
// Programmatic saves may not contain any filters.
if (!isset($format->filters)) {
$format->filters = array();
}
$filter_info = filter_get_filters();
foreach ($filter_info as $name => $filter) {
// Add new filters without weight to the bottom.
if (!isset($format->filters[$name]['weight'])) {
$format->filters[$name]['weight'] = 10;
}
$format->filters[$name]['status'] = isset($format->filters[$name]['status'])
? $format->filters[$name]['status'] : 0;
$format->filters[$name]['module'] = $filter['module'];
// If settings were passed, only ensure default settings.
if (isset($format->filters[$name]['settings'])) {
if (isset($filter['default settings'])) {
$format->filters[$name]['settings'] = array_merge($filter['default setti
ngs'], $format->filters[$name]['settings']);
}
}
// Otherwise, use default settings or fall back to an empty array.
else {
$format->filters[$name]['settings'] = isset($filter['default settings']) ?
$filter['default settings'] : array();
}
$fields = array();
$fields['weight'] =
$fields['status'] =
$fields['module'] =
$fields['settings']

$format->filters[$name]['weight'];
$format->filters[$name]['status'];
$format->filters[$name]['module'];
= serialize($format->filters[$name]['settings']);

db_merge('filter')
->key(array(
'format' => $format->format,
'name' => $name,
))
->fields($fields)
->execute();
}
if ($return == SAVED_NEW) {
module_invoke_all('filter_format_insert', $format);
}
else {
module_invoke_all('filter_format_update', $format);

// Explicitly indicate that the format was updated. We need to do this


// since if the filters were updated but the format object itself was not,
// the merge query above would not return an indication that anything had
// changed.
$return = SAVED_UPDATED;
// Clear the filter cache whenever a text format is updated.
cache_clear_all($format->format . ':', 'cache_filter', TRUE);
}
filter_formats_reset();
return $return;
}
/**
* Disable a text format.
*
* There is no core facility to re-enable a disabled format. It is not deleted
* to keep information for contrib and to make sure the format ID is never
* reused. As there might be content using the disabled format, this would lead
* to data corruption.
*
* @param $format
* The text format object to be disabled.
*/
function filter_format_disable($format) {
db_update('filter_format')
->fields(array('status' => 0))
->condition('format', $format->format)
->execute();
// Allow modules to react on text format deletion.
module_invoke_all('filter_format_disable', $format);
// Clear the filter cache whenever a text format is disabled.
filter_formats_reset();
cache_clear_all($format->format . ':', 'cache_filter', TRUE);
}
/**
* Determines if a text format exists.
*
* @param $format_id
* The ID of the text format to check.
*
* @return
* TRUE if the text format exists, FALSE otherwise. Note that for disabled
* formats filter_format_exists() will return TRUE while filter_format_load()
* will return FALSE.
*
* @see filter_format_load()
*/
function filter_format_exists($format_id) {
return (bool) db_query_range('SELECT 1 FROM {filter_format} WHERE format = :fo
rmat', 0, 1, array(':format' => $format_id))->fetchField();
}
/**
* Display a text format form title.

*/
function filter_admin_format_title($format) {
return $format->name;
}
/**
* Implements hook_permission().
*/
function filter_permission() {
$perms['administer filters'] = array(
'title' => t('Administer text formats and filters'),
'restrict access' => TRUE,
);
// Generate permissions for each text format. Warn the administrator that any
// of them are potentially unsafe.
foreach (filter_formats() as $format) {
$permission = filter_permission_name($format);
if (!empty($permission)) {
// Only link to the text format configuration page if the user who is
// viewing this will have access to that page.
$format_name_replacement = user_access('administer filters') ? l($format->
name, 'admin/config/content/formats/' . $format->format) : drupal_placeholder($f
ormat->name);
$perms[$permission] = array(
'title' => t("Use the !text_format text format", array('!text_format' =>
$format_name_replacement,)),
'description' => drupal_placeholder(t('Warning: This permission may have
security implications depending on how the text format is configured.')),
);
}
}
return $perms;
}
/**
* Returns the machine-readable permission name for a provided text format.
*
* @param $format
* An object representing a text format.
* @return
* The machine-readable permission name, or FALSE if the provided text format
* is malformed or is the fallback format (which is available to all users).
*/
function filter_permission_name($format) {
if (isset($format->format) && $format->format != filter_fallback_format()) {
return 'use text format ' . $format->format;
}
return FALSE;
}
/**
* Implements hook_modules_enabled().
*/
function filter_modules_enabled($modules) {
// Reset the static cache of module-provided filters, in case any of the
// newly enabled modules defines a new filter or alters existing ones.
drupal_static_reset('filter_get_filters');
}

/**
* Implements hook_modules_disabled().
*/
function filter_modules_disabled($modules) {
// Reset the static cache of module-provided filters, in case any of the
// newly disabled modules defined or altered any filters.
drupal_static_reset('filter_get_filters');
}
/**
* Retrieve a list of text formats, ordered by weight.
*
* @param $account
* (optional) If provided, only those formats that are allowed for this user
* account will be returned. All formats will be returned otherwise.
* @return
* An array of text format objects, keyed by the format ID and ordered by
* weight.
*
* @see filter_formats_reset()
*/
function filter_formats($account = NULL) {
global $language;
$formats = &drupal_static(__FUNCTION__, array());
// All available formats are cached for performance.
if (!isset($formats['all'])) {
if ($cache = cache_get("filter_formats:{$language->language}")) {
$formats['all'] = $cache->data;
}
else {
$formats['all'] = db_select('filter_format', 'ff')
->addTag('translatable')
->fields('ff')
->condition('status', 1)
->orderBy('weight')
->execute()
->fetchAllAssoc('format');
cache_set("filter_formats:{$language->language}", $formats['all']);
}
}
// Build a list of user-specific formats.
if (isset($account) && !isset($formats['user'][$account->uid])) {
$formats['user'][$account->uid] = array();
foreach ($formats['all'] as $format) {
if (filter_access($format, $account)) {
$formats['user'][$account->uid][$format->format] = $format;
}
}
}
return isset($account) ? $formats['user'][$account->uid] : $formats['all'];
}
/**
* Resets text format caches.
*
* @see filter_formats()

*/
function filter_formats_reset() {
cache_clear_all('filter_formats', 'cache', TRUE);
cache_clear_all('filter_list_format', 'cache', TRUE);
drupal_static_reset('filter_list_format');
drupal_static_reset('filter_formats');
}
/**
* Retrieves a list of roles that are allowed to use a given text format.
*
* @param $format
* An object representing the text format.
* @return
* An array of role names, keyed by role ID.
*/
function filter_get_roles_by_format($format) {
// Handle the fallback format upfront (all roles have access to this format).
if ($format->format == filter_fallback_format()) {
return user_roles();
}
// Do not list any roles if the permission does not exist.
$permission = filter_permission_name($format);
return !empty($permission) ? user_roles(FALSE, $permission) : array();
}
/**
* Retrieves a list of text formats that are allowed for a given role.
*
* @param $rid
* The user role ID to retrieve text formats for.
* @return
* An array of text format objects that are allowed for the role, keyed by
* the text format ID and ordered by weight.
*/
function filter_get_formats_by_role($rid) {
$formats = array();
foreach (filter_formats() as $format) {
$roles = filter_get_roles_by_format($format);
if (isset($roles[$rid])) {
$formats[$format->format] = $format;
}
}
return $formats;
}
/**
* Returns the ID of the default text format for a particular user.
*
* The default text format is the first available format that the user is
* allowed to access, when the formats are ordered by weight. It should
* generally be used as a default choice when presenting the user with a list
* of possible text formats (for example, in a node creation form).
*
* Conversely, when existing content that does not have an assigned text format
* needs to be filtered for display, the default text format is the wrong
* choice, because it is not guaranteed to be consistent from user to user, and
* some trusted users may have an unsafe text format set by default, which
* should not be used on text of unknown origin. Instead, the fallback format
* returned by filter_fallback_format() should be used, since that is intended

* to be a safe, consistent format that is always available to all users.


*
* @param $account
* (optional) The user account to check. Defaults to the currently logged-in
* user.
* @return
* The ID of the user's default text format.
*
* @see filter_fallback_format()
*/
function filter_default_format($account = NULL) {
global $user;
if (!isset($account)) {
$account = $user;
}
// Get a list of formats for this user, ordered by weight. The first one
// available is the user's default format.
$formats = filter_formats($account);
$format = reset($formats);
return $format->format;
}
/**
* Returns the ID of the fallback text format that all users have access to.
*
* The fallback text format is a regular text format in every respect, except
* it does not participate in the filter permission system and cannot be
* disabled. It needs to exist because any user who has permission to create
* formatted content must always have at least one text format they can use.
*
* Because the fallback format is available to all users, it should always be
* configured securely. For example, when the Filter module is installed, this
* format is initialized to output plain text. Installation profiles and site
* administrators have the freedom to configure it further.
*
* Note that the fallback format is completely distinct from the default
* format, which differs per user and is simply the first format which that
* user has access to. The default and fallback formats are only guaranteed to
* be the same for users who do not have access to any other format; otherwise,
* the fallback format's weight determines its placement with respect to the
* user's other formats.
*
* Any modules implementing a format deletion functionality must not delete
* this format.
*
* @see hook_filter_format_disable()
* @see filter_default_format()
*/
function filter_fallback_format() {
// This variable is automatically set in the database for all installations
// of Drupal. In the event that it gets disabled or deleted somehow, there
// is no safe default to return, since we do not want to risk making an
// existing (and potentially unsafe) text format on the site automatically
// available to all users. Returning NULL at least guarantees that this
// cannot happen.
return variable_get('filter_fallback_format');
}
/**
* Returns the title of the fallback text format.

*/
function filter_fallback_format_title() {
$fallback_format = filter_format_load(filter_fallback_format());
return filter_admin_format_title($fallback_format);
}
/**
* Return a list of all filters provided by modules.
*/
function filter_get_filters() {
$filters = &drupal_static(__FUNCTION__, array());
if (empty($filters)) {
foreach (module_implements('filter_info') as $module) {
$info = module_invoke($module, 'filter_info');
if (isset($info) && is_array($info)) {
// Assign the name of the module implementing the filters and ensure
// default values.
foreach (array_keys($info) as $name) {
$info[$name]['module'] = $module;
$info[$name] += array(
'description' => '',
'weight' => 0,
);
}
$filters = array_merge($filters, $info);
}
}
// Allow modules to alter filter definitions.
drupal_alter('filter_info', $filters);
uasort($filters, '_filter_list_cmp');
}
return $filters;
}
/**
* Helper function for sorting the filter list by filter name.
*/
function _filter_list_cmp($a, $b) {
return strcmp($a['title'], $b['title']);
}
/**
* Check if text in a certain text format is allowed to be cached.
*
* This function can be used to check whether the result of the filtering
* process can be cached. A text format may allow caching depending on the
* filters enabled.
*
* @param $format_id
* The text format ID to check.
* @return
* TRUE if the given text format allows caching, FALSE otherwise.
*/
function filter_format_allowcache($format_id) {
$format = filter_format_load($format_id);
return !empty($format->cache);
}

/**
* Helper function to determine whether the output of a given text format can be
cached.
*
* The output of a given text format can be cached when all enabled filters in
* the text format allow caching.
*
* @param $format
* The text format object to check.
* @return
* TRUE if all the filters enabled in the given text format allow caching,
* FALSE otherwise.
*
* @see filter_format_save()
*/
function _filter_format_is_cacheable($format) {
if (empty($format->filters)) {
return TRUE;
}
$filter_info = filter_get_filters();
foreach ($format->filters as $name => $filter) {
// By default, 'cache' is TRUE for all filters unless specified otherwise.
if (!empty($filter['status']) && isset($filter_info[$name]['cache']) && !$fi
lter_info[$name]['cache']) {
return FALSE;
}
}
return TRUE;
}
/**
* Retrieve a list of filters for a given text format.
*
* Note that this function returns all associated filters regardless of whether
* they are enabled or disabled. All functions working with the filter
* information outside of filter administration should test for $filter->status
* before performing actions with the filter.
*
* @param $format_id
* The format ID to retrieve filters for.
*
* @return
* An array of filter objects associated to the given text format, keyed by
* filter name.
*/
function filter_list_format($format_id) {
$filters = &drupal_static(__FUNCTION__, array());
$filter_info = filter_get_filters();
if (!isset($filters['all'])) {
if ($cache = cache_get('filter_list_format')) {
$filters['all'] = $cache->data;
}
else {
$result = db_query('SELECT * FROM {filter} ORDER BY weight, module, name')
;
foreach ($result as $record) {
$filters['all'][$record->format][$record->name] = $record;
}

cache_set('filter_list_format', $filters['all']);
}
}
if (!isset($filters[$format_id])) {
$format_filters = array();
foreach ($filters['all'][$format_id] as $name => $filter) {
if (isset($filter_info[$name])) {
$filter->title = $filter_info[$name]['title'];
// Unpack stored filter settings.
$filter->settings = (isset($filter->settings) ? unserialize($filter->set
tings) : array());
// Merge in default settings.
if (isset($filter_info[$name]['default settings'])) {
$filter->settings += $filter_info[$name]['default settings'];
}
$format_filters[$name] = $filter;
}
}
$filters[$format_id] = $format_filters;
}
return isset($filters[$format_id]) ? $filters[$format_id] : array();
}
/**
* Run all the enabled filters on a piece of text.
*
* Note: Because filters can inject JavaScript or execute PHP code, security is
* vital here. When a user supplies a text format, you should validate it using
* filter_access() before accepting/using it. This is normally done in the
* validation stage of the Form API. You should for example never make a preview
* of content in a disallowed format.
*
* @param $text
* The text to be filtered.
* @param $format_id
* The format id of the text to be filtered. If no format is assigned, the
* fallback format will be used.
* @param $langcode
* Optional: the language code of the text to be filtered, e.g. 'en' for
* English. This allows filters to be language aware so language specific
* text replacement can be implemented.
* @param $cache
* Boolean whether to cache the filtered output in the {cache_filter} table.
* The caller may set this to FALSE when the output is already cached
* elsewhere to avoid duplicate cache lookups and storage.
*
* @ingroup sanitization
*/
function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE)
{
if (!isset($format_id)) {
$format_id = filter_fallback_format();
}
// If the requested text format does not exist, the text cannot be filtered.
if (!$format = filter_format_load($format_id)) {
watchdog('filter', 'Missing text format: %format.', array('%format' => $form
at_id), WATCHDOG_ALERT);

return '';
}
// Check for a cached version of this piece of text.
$cache = $cache && !empty($format->cache);
$cache_id = '';
if ($cache) {
$cache_id = $format->format . ':' . $langcode . ':' . hash('sha256', $text);
if ($cached = cache_get($cache_id, 'cache_filter')) {
return $cached->data;
}
}
// Convert all Windows and Mac newlines to a single newline, so filters only
// need to deal with one possibility.
$text = str_replace(array("\r\n", "\r"), "\n", $text);
// Get a complete list of filters, ordered properly.
$filters = filter_list_format($format->format);
$filter_info = filter_get_filters();
// Give filters the chance to escape HTML-like data such as code or formulas.
foreach ($filters as $name => $filter) {
if ($filter->status && isset($filter_info[$name]['prepare callback']) && fun
ction_exists($filter_info[$name]['prepare callback'])) {
$function = $filter_info[$name]['prepare callback'];
$text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
}
}
// Perform filtering.
foreach ($filters as $name => $filter) {
if ($filter->status && isset($filter_info[$name]['process callback']) && fun
ction_exists($filter_info[$name]['process callback'])) {
$function = $filter_info[$name]['process callback'];
$text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
}
}
// Store in cache with a minimum expiration time of 1 day.
if ($cache) {
cache_set($cache_id, $text, 'cache_filter', REQUEST_TIME + (60 * 60 * 24));
}
return $text;
}
/**
* Expands an element into a base element with text format selector attached.
*
* The form element will be expanded into two separate form elements, one
* holding the original element, and the other holding the text format selector:
* - value: Holds the original element, having its #type changed to the value of
* #base_type or 'textarea' by default.
* - format: Holds the text format fieldset and the text format selection, using
* the text format id specified in #format or the user's default format by
* default, if NULL.
*
* The resulting value for the element will be an array holding the value and th
e

* format. For example, the value for the body element will be:
* @code
* $form_state['values']['body']['value'] = 'foo';
* $form_state['values']['body']['format'] = 'foo';
* @endcode
*
* @param $element
* The form element to process. Properties used:
* - #base_type: The form element #type to use for the 'value' element.
*
'textarea' by default.
* - #format: (optional) The text format id to preselect. If NULL or not set,
*
the default format for the current user will be used.
*
* @return
* The expanded element.
*/
function filter_process_format($element) {
global $user;
// Ensure that children appear as subkeys of this element.
$element['#tree'] = TRUE;
$blacklist = array(
// Make form_builder() regenerate child properties.
'#parents',
'#id',
'#name',
// Do not copy this #process function to prevent form_builder() from
// recursing infinitely.
'#process',
// Description is handled by theme_text_format_wrapper().
'#description',
// Ensure proper ordering of children.
'#weight',
// Properties already processed for the parent element.
'#prefix',
'#suffix',
'#attached',
'#processed',
'#theme_wrappers',
);
// Move this element into sub-element 'value'.
unset($element['value']);
foreach (element_properties($element) as $key) {
if (!in_array($key, $blacklist)) {
$element['value'][$key] = $element[$key];
}
}
$element['value']['#type'] = $element['#base_type'];
$element['value'] += element_info($element['#base_type']);
// Turn original element into a text format wrapper.
$path = drupal_get_path('module', 'filter');
$element['#attached']['js'][] = $path . '/filter.js';
$element['#attached']['css'][] = $path . '/filter.css';
// Setup child container for the text format widget.
$element['format'] = array(
'#type' => 'fieldset',
'#attributes' => array('class' => array('filter-wrapper')),

);
// Prepare text format guidelines.
$element['format']['guidelines'] = array(
'#type' => 'container',
'#attributes' => array('class' => array('filter-guidelines')),
'#weight' => 20,
);
// Get a list of formats that the current user has access to.
$formats = filter_formats($user);
foreach ($formats as $format) {
$options[$format->format] = $format->name;
$element['format']['guidelines'][$format->format] = array(
'#theme' => 'filter_guidelines',
'#format' => $format,
);
}
// Use the default format for this user if none was selected.
if (!isset($element['#format'])) {
$element['#format'] = filter_default_format($user);
}
$element['format']['format'] = array(
'#type' => 'select',
'#title' => t('Text format'),
'#options' => $options,
'#default_value' => $element['#format'],
'#access' => count($formats) > 1,
'#weight' => 10,
'#attributes' => array('class' => array('filter-list')),
'#parents' => array_merge($element['#parents'], array('format')),
);
$element['format']['help'] = array(
'#type' => 'container',
'#theme' => 'filter_tips_more_info',
'#attributes' => array('class' => array('filter-help')),
'#weight' => 0,
);
$all_formats = filter_formats();
$format_exists = isset($all_formats[$element['#format']]);
$user_has_access = isset($formats[$element['#format']]);
$user_is_admin = user_access('administer filters');
// If the stored format does not exist, administrators have to assign a new
// format.
if (!$format_exists && $user_is_admin) {
$element['format']['format']['#required'] = TRUE;
$element['format']['format']['#default_value'] = NULL;
// Force access to the format selector (it may have been denied above if
// the user only has access to a single format).
$element['format']['format']['#access'] = TRUE;
}
// Disable this widget, if the user is not allowed to use the stored format,
// or if the stored format does not exist. The 'administer filters' permission
// only grants access to the filter administration, not to all formats.
elseif (!$user_has_access || !$format_exists) {
// Overload default values into #value to make them unalterable.

$element['value']['#value'] = $element['value']['#default_value'];
$element['format']['format']['#value'] = $element['format']['format']['#defa
ult_value'];
// Prepend #pre_render callback to replace field value with user notice
// prior to rendering.
$element['value'] += array('#pre_render' => array());
array_unshift($element['value']['#pre_render'], 'filter_form_access_denied')
;
// Cosmetic adjustments.
if (isset($element['value']['#rows'])) {
$element['value']['#rows'] = 3;
}
$element['value']['#disabled'] = TRUE;
$element['value']['#resizable'] = FALSE;
// Hide the text format selector and any other child element (such as text
// field's summary).
foreach (element_children($element) as $key) {
if ($key != 'value') {
$element[$key]['#access'] = FALSE;
}
}
}
return $element;
}
/**
* #pre_render callback for #type 'text_format' to hide field value from prying
eyes.
*
* To not break form processing and previews if a user does not have access to a
* stored text format, the expanded form elements in filter_process_format() are
* forced to take over the stored #default_values for 'value' and 'format'.
* However, to prevent the unfiltered, original #value from being displayed to
* the user, we replace it with a friendly notice here.
*
* @see filter_process_format()
*/
function filter_form_access_denied($element) {
$element['#value'] = t('This field has been disabled because you do not have s
ufficient permissions to edit it.');
return $element;
}
/**
* Returns HTML for a text format-enabled form element.
*
* @param $variables
* An associative array containing:
* - element: A render element containing #children and #description.
*
* @ingroup themeable
*/
function theme_text_format_wrapper($variables) {
$element = $variables['element'];
$output = '<div class="text-format-wrapper">';
$output .= $element['#children'];

if (!empty($element['#description'])) {
$output .= '<div class="description">' . $element['#description'] . '</div>'
;
}
$output .= "</div>\n";
return $output;
}
/**
* Checks if a user has access to a particular text format.
*
* @param $format
* An object representing the text format.
* @param $account
* (optional) The user account to check access for; if omitted, the currently
* logged-in user is used.
*
* @return
* Boolean TRUE if the user is allowed to access the given format.
*/
function filter_access($format, $account = NULL) {
global $user;
if (!isset($account)) {
$account = $user;
}
// Handle special cases up front. All users have access to the fallback
// format.
if ($format->format == filter_fallback_format()) {
return TRUE;
}
// Check the permission if one exists; otherwise, we have a non-existent
// format so we return FALSE.
$permission = filter_permission_name($format);
return !empty($permission) && user_access($permission, $account);
}
/**
* Helper function for fetching filter tips.
*/
function _filter_tips($format_id, $long = FALSE) {
global $user;
$formats = filter_formats($user);
$filter_info = filter_get_filters();
$tips = array();
// If only listing one format, extract it from the $formats array.
if ($format_id != -1) {
$formats = array($formats[$format_id]);
}
foreach ($formats as $format) {
$filters = filter_list_format($format->format);
$tips[$format->name] = array();
foreach ($filters as $name => $filter) {
if ($filter->status && isset($filter_info[$name]['tips callback']) && func
tion_exists($filter_info[$name]['tips callback'])) {
$tip = $filter_info[$name]['tips callback']($filter, $format, $long);

if (isset($tip)) {
$tips[$format->name][$name] = array('tip' => $tip, 'id' => $name);
}
}
}
}
return $tips;
}
/**
* Parses an HTML snippet and returns it as a DOM object.
*
* This function loads the body part of a partial (X)HTML document
* and returns a full DOMDocument object that represents this document.
* You can use filter_dom_serialize() to serialize this DOMDocument
* back to a XHTML snippet.
*
* @param $text
* The partial (X)HTML snippet to load. Invalid mark-up
* will be corrected on import.
* @return
* A DOMDocument that represents the loaded (X)HTML snippet.
*/
function filter_dom_load($text) {
$dom_document = new DOMDocument();
// Ignore warnings during HTML soup loading.
@$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; ch
arset=utf-8" /></head><body>' . $text . '</body></html>');
return $dom_document;
}
/**
* Converts a DOM object back to an HTML snippet.
*
* The function serializes the body part of a DOMDocument
* back to an XHTML snippet.
*
* The resulting XHTML snippet will be properly formatted
* to be compatible with HTML user agents.
*
* @param $dom_document
* A DOMDocument object to serialize, only the tags below
* the first <body> node will be converted.
* @return
* A valid (X)HTML snippet, as a string.
*/
function filter_dom_serialize($dom_document) {
$body_node = $dom_document->getElementsByTagName('body')->item(0);
$body_content = '';
foreach ($body_node->getElementsByTagName('script') as $node) {
filter_dom_serialize_escape_cdata_element($dom_document, $node);
}
foreach ($body_node->getElementsByTagName('style') as $node) {
filter_dom_serialize_escape_cdata_element($dom_document, $node, '/*', '*/');

}
foreach ($body_node->childNodes as $child_node) {
$body_content .= $dom_document->saveXML($child_node);
}
return preg_replace('|<([^> ]*)/>|i', '<$1 />', $body_content);
}
/**
* Adds comments around the <!CDATA section in a dom element.
*
* DOMDocument::loadHTML in filter_dom_load() makes CDATA sections from the
* contents of inline script and style tags. This can cause HTML 4 browsers to
* throw exceptions.
*
* This function attempts to solve the problem by creating a DocumentFragment
* and immitating the behavior in drupal_get_js(), commenting the CDATA tag.
*
* @param $dom_document
* The DOMDocument containing the $dom_element.
* @param $dom_element
* The element potentially containing a CDATA node.
* @param $comment_start
* String to use as a comment start marker to escape the CDATA declaration.
* @param $comment_end
* String to use as a comment end marker to escape the CDATA declaration.
*/
function filter_dom_serialize_escape_cdata_element($dom_document, $dom_element,
$comment_start = '//', $comment_end = '') {
foreach ($dom_element->childNodes as $node) {
if (get_class($node) == 'DOMCdataSection') {
// See drupal_get_js(). This code is more or less duplicated there.
$embed_prefix = "\n<!--{$comment_start}--><![CDATA[{$comment_start} ><!--{
$comment_end}\n";
$embed_suffix = "\n{$comment_start}--><!]]>{$comment_end}\n";
if (preg_match('/[!--<>\/* ]+\<\!\[CDATA\[/', $node->data)) {
$embed_prefix = NULL;
}
if (preg_match('/[!--<>\/* ]+\]\]\>/', $node->data)) {
$embed_suffix = NULL;
}
$fragment = $dom_document->createDocumentFragment();
$fragment->appendXML($embed_prefix . $node->data . $embed_suffix);
$dom_element->appendChild($fragment);
}
}
}
/**
* Returns HTML for a link to the more extensive filter tips.
*
* @ingroup themeable
*/
function theme_filter_tips_more_info() {
return '<p>' . l(t('More information about text formats'), 'filter/tips') . '<
/p>';
}
/**

* Returns HTML for guidelines for a text format.


*
* @param $variables
* An associative array containing:
* - format: An object representing a text format.
*
* @ingroup themeable
*/
function theme_filter_guidelines($variables) {
$format = $variables['format'];
$attributes['class'][] = 'filter-guidelines-item';
$attributes['class'][] = 'filter-guidelines-' . $format->format;
$output = '<div' . drupal_attributes($attributes) . '>';
$output .= '<h3>' . check_plain($format->name) . '</h3>';
$output .= theme('filter_tips', array('tips' => _filter_tips($format->format,
FALSE)));
$output .= '</div>';
return $output;
}
/**
* @defgroup standard_filters Standard filters
* @{
* Filters implemented by the filter.module.
*/
/**
* Implements hook_filter_info().
*/
function filter_filter_info() {
$filters['filter_html'] = array(
'title' => t('Limit allowed HTML tags'),
'process callback' => '_filter_html',
'settings callback' => '_filter_html_settings',
'default settings' => array(
'allowed_html' => '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol>
<li> <dl> <dt> <dd>',
'filter_html_help' => 1,
'filter_html_nofollow' => 0,
),
'tips callback' => '_filter_html_tips',
'weight' => -10,
);
$filters['filter_autop'] = array(
'title' => t('Convert line breaks into HTML (i.e. <code>&lt;br&gt;</code> an
d <code>&lt;p&gt;</code>)'),
'process callback' => '_filter_autop',
'tips callback' => '_filter_autop_tips',
);
$filters['filter_url'] = array(
'title' => t('Convert URLs into links'),
'process callback' => '_filter_url',
'settings callback' => '_filter_url_settings',
'default settings' => array(
'filter_url_length' => 72,
),
'tips callback' => '_filter_url_tips',
);
$filters['filter_htmlcorrector'] = array(
'title' => t('Correct faulty and chopped off HTML'),

'process callback' => '_filter_htmlcorrector',


'weight' => 10,
);
$filters['filter_html_escape'] = array(
'title' => t('Display any HTML as plain text'),
'process callback' => '_filter_html_escape',
'tips callback' => '_filter_html_escape_tips',
'weight' => -10,
);
return $filters;
}
/**
* Settings callback for the HTML filter.
*/
function _filter_html_settings($form, &$form_state, $filter, $format, $defaults)
{
$filter->settings += $defaults;
$settings['allowed_html'] = array(
'#type' => 'textfield',
'#title' => t('Allowed HTML tags'),
'#default_value' => $filter->settings['allowed_html'],
'#maxlength' => 1024,
'#description' => t('A list of HTML tags that can be used. JavaScript event
attributes, JavaScript URLs, and CSS are always stripped.'),
);
$settings['filter_html_help'] = array(
'#type' => 'checkbox',
'#title' => t('Display basic HTML help in long filter tips'),
'#default_value' => $filter->settings['filter_html_help'],
);
$settings['filter_html_nofollow'] = array(
'#type' => 'checkbox',
'#title' => t('Add rel="nofollow" to all links'),
'#default_value' => $filter->settings['filter_html_nofollow'],
);
return $settings;
}
/**
* HTML filter. Provides filtering of input into accepted HTML.
*/
function _filter_html($text, $filter) {
$allowed_tags = preg_split('/\s+|<|>/', $filter->settings['allowed_html'], -1,
PREG_SPLIT_NO_EMPTY);
$text = filter_xss($text, $allowed_tags);
if ($filter->settings['filter_html_nofollow']) {
$html_dom = filter_dom_load($text);
$links = $html_dom->getElementsByTagName('a');
foreach ($links as $link) {
$link->setAttribute('rel', 'nofollow');
}
$text = filter_dom_serialize($html_dom);
}
return trim($text);
}

/**
* Filter tips callback for HTML filter.
*/
function _filter_html_tips($filter, $format, $long = FALSE) {
global $base_url;
if (!($allowed_html = $filter->settings['allowed_html'])) {
return;
}
$output = t('Allowed HTML tags: @tags', array('@tags' => $allowed_html));
if (!$long) {
return $output;
}
$output = '<p>' . $output . '</p>';
if (!$filter->settings['filter_html_help']) {
return $output;
}
$output .= '<p>' . t('This site allows HTML content. While learning all of HTM
L may feel intimidating, learning how to use a very small number of the most bas
ic HTML "tags" is very easy. This table provides examples for each tag that is e
nabled on this site.') . '</p>';
$output .= '<p>' . t('For more information see W3C\'s <a href="@html-specifica
tions">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>';
$tips = array(
'a' => array(t('Anchors are used to make links to other pages.'), '<a href="
' . $base_url . '">' . check_plain(variable_get('site_name', 'Drupal')) . '</a>'
),
'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')),
'p' => array(t('By default paragraph tags are automatically added, so use th
is tag to add additional ones.'), '<p>' . t('Paragraph one.') . '</p> <p>' . t('
Paragraph two.') . '</p>'),
'strong' => array(t('Strong', array(), array('context' => 'Font weight')), '
<strong>' . t('Strong', array(), array('context' => 'Font weight')) . '</strong>
'),
'em' => array(t('Emphasized'), '<em>' . t('Emphasized') . '</em>'),
'cite' => array(t('Cited'), '<cite>' . t('Cited') . '</cite>'),
'code' => array(t('Coded text used to show programming source code'), '<code
>' . t('Coded') . '</code>'),
'b' => array(t('Bolded'), '<b>' . t('Bolded') . '</b>'),
'u' => array(t('Underlined'), '<u>' . t('Underlined') . '</u>'),
'i' => array(t('Italicized'), '<i>' . t('Italicized') . '</i>'),
'sup' => array(t('Superscripted'), t('<sup>Super</sup>scripted')),
'sub' => array(t('Subscripted'), t('<sub>Sub</sub>scripted')),
'pre' => array(t('Preformatted'), '<pre>' . t('Preformatted') . '</pre>'),
'abbr' => array(t('Abbreviation'), t('<abbr title="Abbreviation">Abbrev.</ab
br>')),
'acronym' => array(t('Acronym'), t('<acronym title="Three-Letter Acronym">TL
A</acronym>')),
'blockquote' => array(t('Block quoted'), '<blockquote>' . t('Block quoted')
. '</blockquote>'),
'q' => array(t('Quoted inline'), '<q>' . t('Quoted inline') . '</q>'),
// Assumes and describes tr, td, th.
'table' => array(t('Table'), '<table> <tr><th>' . t('Table header') . '</th>

</tr> <tr><td>' . t('Table cell') . '</td></tr> </table>'),


'tr' => NULL, 'td' => NULL, 'th' => NULL,
'del' => array(t('Deleted'), '<del>' . t('Deleted') . '</del>'),
'ins' => array(t('Inserted'), '<ins>' . t('Inserted') . '</ins>'),
// Assumes and describes li.
'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>
'),
'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> </u
l>'),
'li' => NULL,
// Assumes and describes dt and dd.
'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&g
t; 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>'),
'dt' => NULL, 'dd' => NULL,
'h1' => array(t('Heading'), '<h1>' . t('Title') . '</h1>'),
'h2' => array(t('Heading'), '<h2>' . t('Subtitle') . '</h2>'),
'h3' => array(t('Heading'), '<h3>' . t('Subtitle three') . '</h3>'),
'h4' => array(t('Heading'), '<h4>' . t('Subtitle four') . '</h4>'),
'h5' => array(t('Heading'), '<h5>' . t('Subtitle five') . '</h5>'),
'h6' => array(t('Heading'), '<h6>' . t('Subtitle six') . '</h6>')
);
$header = array(t('Tag Description'), t('You Type'), t('You Get'));
preg_match_all('/<([a-z0-9]+)[^a-z0-9]/i', $allowed_html, $out);
foreach ($out[1] as $tag) {
if (!empty($tips[$tag])) {
$rows[] = array(
array('data' => $tips[$tag][0], 'class' => array('description')),
array('data' => '<code>' . check_plain($tips[$tag][1]) . '</code>', 'cla
ss' => array('type')),
array('data' => $tips[$tag][1], 'class' => array('get'))
);
}
else {
$rows[] = array(
array('data' => t('No help provided for tag %tag.', array('%tag' => $tag
)), 'class' => array('description'), 'colspan' => 3),
);
}
}
$output .= theme('table', array('header' => $header, 'rows' => $rows));
$output .= '<p>' . t('Most unusual characters can be directly entered without
any problems.') . '</p>';
$output .= '<p>' . t('If you do encounter problems, try using HTML character e
ntities. 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> p
age. Some of the available characters include:', array('@html-entities' => 'http
://www.w3.org/TR/html4/sgml/entities.html')) . '</p>';
$entities = array(
array(t('Ampersand'), '&amp;'),
array(t('Greater than'), '&gt;'),
array(t('Less than'), '&lt;'),
array(t('Quotation mark'), '&quot;'),
);

$header = array(t('Character Description'), t('You Type'), t('You Get'));


unset($rows);
foreach ($entities as $entity) {
$rows[] = array(
array('data' => $entity[0], 'class' => array('description')),
array('data' => '<code>' . check_plain($entity[1]) . '</code>', 'class' =>
array('type')),
array('data' => $entity[1], 'class' => array('get'))
);
}
$output .= theme('table', array('header' => $header, 'rows' => $rows));
return $output;
}
/**
* Settings callback for URL filter.
*/
function _filter_url_settings($form, &$form_state, $filter, $format, $defaults)
{
$filter->settings += $defaults;
$settings['filter_url_length'] = array(
'#type' => 'textfield',
'#title' => t('Maximum link text length'),
'#default_value' => $filter->settings['filter_url_length'],
'#size' => 5,
'#maxlength' => 4,
'#field_suffix' => t('characters'),
'#description' => t('URLs longer than this number of characters will be trun
cated to prevent long strings that break formatting. The link itself will be ret
ained; just the text portion of the link will be truncated.'),
);
return $settings;
}
/**
* URL filter. Automatically converts text into hyperlinks.
*
* This filter identifies and makes clickable three types of "links".
* - URLs like http://example.com.
* - E-mail addresses like name@example.com.
* - Web addresses without the "http://" protocol defined, like www.example.com.
* Each type must be processed separately, as there is no one regular
* expression that could possibly match all of the cases in one pass.
*/
function _filter_url($text, $filter) {
// Tags to skip and not recurse into.
$ignore_tags = 'a|script|style|code|pre';
// Pass length to regexp callback.
_filter_url_trim(NULL, $filter->settings['filter_url_length']);
// Create an array which contains the regexps for each type of link.
// The key to the regexp is the name of a function that is used as
// callback function to process matches of the regexp. The callback function
// is to return the replacement for the match. The array is used and
// matching/replacement done below inside some loops.
$tasks = array();
// Prepare protocols pattern for absolute URLs.

// check_url() will replace any bad protocols with HTTP, so we need to support
// the identical list. While '//' is technically optional for MAILTO only,
// we cannot cleanly differ between protocols here without hard-coding MAILTO,
// so '//' is optional for all protocols.
// @see filter_xss_bad_protocol()
$protocols = variable_get('filter_allowed_protocols', array('http', 'https', '
ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal', 'rtsp'
));
$protocols = implode(':(?://)?|', $protocols) . ':(?://)?';
// Prepare domain name pattern.
// The ICANN seems to be on track towards accepting more diverse top level
// domains, so this pattern has been "future-proofed" to allow for TLDs
// of length 2-64.
$domain = '(?:[A-Za-z0-9._+-]+\.)?[A-Za-z]{2,64}\b';
$ip = '(?:[0-9]{1,3}\.){3}[0-9]{1,3}';
$auth = '[a-zA-Z0-9:%_+*~#?&=.,/;-]+@';
$trail = '[a-zA-Z0-9:%_+*~#&\[\]=/;?!\.,-]*[a-zA-Z0-9:%_+*~#&\[\]=/;-]';
// Prepare pattern for optional trailing punctuation.
// Even these characters could have a valid meaning for the URL, such usage is
// rare compared to using a URL at the end of or within a sentence, so these
// trailing characters are optionally excluded.
$punctuation = '[\.,?!]*?';
// Match absolute URLs.
$url_pattern = "(?:$auth)?(?:$domain|$ip)/?(?:$trail)?";
$pattern = "`((?:$protocols)(?:$url_pattern))($punctuation)`";
$tasks['_filter_url_parse_full_links'] = $pattern;
// Match e-mail addresses.
$url_pattern = "[A-Za-z0-9._-]+@(?:$domain)";
$pattern = "`($url_pattern)`";
$tasks['_filter_url_parse_email_links'] = $pattern;
// Match www domains.
$url_pattern = "www\.(?:$domain)/?(?:$trail)?";
$pattern = "`($url_pattern)($punctuation)`";
$tasks['_filter_url_parse_partial_links'] = $pattern;
// Each type of URL needs to be processed separately. The text is joined and
// re-split after each task, since all injected HTML tags must be correctly
// protected before the next task.
foreach ($tasks as $task => $pattern) {
// HTML comments need to be handled separately, as they may contain HTML
// markup, especially a '>'. Therefore, remove all comment contents and add
// them back later.
_filter_url_escape_comments('', TRUE);
$text = preg_replace_callback('`<!--(.*?)-->`s', '_filter_url_escape_comment
s', $text);
// Split at all tags; ensures that no tags or attributes are processed.
$chunks = preg_split('/(<.+?>)/is', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
// PHP ensures that the array consists of alternating delimiters and
// literals, and begins and ends with a literal (inserting NULL as
// required). Therefore, the first chunk is always text:
$chunk_type = 'text';
// If a tag of $ignore_tags is found, it is stored in $open_tag and only
// removed when the closing tag is found. Until the closing tag is found,
// no replacements are made.

$open_tag = '';
for ($i = 0; $i < count($chunks); $i++) {
if ($chunk_type == 'text') {
// Only process this text if there are no unclosed $ignore_tags.
if ($open_tag == '') {
// If there is a match, inject a link into this chunk via the callback
// function contained in $task.
$chunks[$i] = preg_replace_callback($pattern, $task, $chunks[$i]);
}
// Text chunk is done, so next chunk must be a tag.
$chunk_type = 'tag';
}
else {
// Only process this tag if there are no unclosed $ignore_tags.
if ($open_tag == '') {
// Check whether this tag is contained in $ignore_tags.
if (preg_match("`<($ignore_tags)(?:\s|>)`i", $chunks[$i], $matches)) {
$open_tag = $matches[1];
}
}
// Otherwise, check whether this is the closing tag for $open_tag.
else {
if (preg_match("`<\/$open_tag>`i", $chunks[$i], $matches)) {
$open_tag = '';
}
}
// Tag chunk is done, so next chunk must be text.
$chunk_type = 'text';
}
}
$text = implode($chunks);
// Revert back to the original comment contents
_filter_url_escape_comments('', FALSE);
$text = preg_replace_callback('`<!--(.*?)-->`', '_filter_url_escape_comments
', $text);
}
return $text;
}
/**
* preg_replace callback to make links out of absolute URLs.
*/
function _filter_url_parse_full_links($match) {
// The $i:th parenthesis in the regexp contains the URL.
$i = 1;
$match[$i]
$caption =
$match[$i]
return '<a

= decode_entities($match[$i]);
check_plain(_filter_url_trim($match[$i]));
= check_plain($match[$i]);
href="' . $match[$i] . '">' . $caption . '</a>' . $match[$i + 1];

}
/**
* preg_replace callback to make links out of e-mail addresses.
*/
function _filter_url_parse_email_links($match) {
// The $i:th parenthesis in the regexp contains the URL.

$i = 0;
$match[$i]
$caption =
$match[$i]
return '<a

= decode_entities($match[$i]);
check_plain(_filter_url_trim($match[$i]));
= check_plain($match[$i]);
href="mailto:' . $match[$i] . '">' . $caption . '</a>';

}
/**
* preg_replace callback to make links out of domain names starting with "www."
*/
function _filter_url_parse_partial_links($match) {
// The $i:th parenthesis in the regexp contains the URL.
$i = 1;
$match[$i]
$caption =
$match[$i]
return '<a
+ 1];
}

= decode_entities($match[$i]);
check_plain(_filter_url_trim($match[$i]));
= check_plain($match[$i]);
href="http://' . $match[$i] . '">' . $caption . '</a>' . $match[$i

/**
* preg_replace callback to escape contents of HTML comments
*
* @param $match
* An array containing matches to replace from preg_replace_callback(),
* whereas $match[1] is expected to contain the content to be filtered.
* @param $escape
* (optional) Boolean whether to escape (TRUE) or unescape comments (FALSE).
* Defaults to neither. If TRUE, statically cached $comments are reset.
*/
function _filter_url_escape_comments($match, $escape = NULL) {
static $mode, $comments = array();
if (isset($escape)) {
$mode = $escape;
if ($escape){
$comments = array();
}
return;
}
// Replace all HTML coments with a '<!-- [hash] -->' placeholder.
if ($mode) {
$content = $match[1];
$hash = md5($content);
$comments[$hash] = $content;
return "<!-- $hash -->";
}
// Or replace placeholders with actual comment contents.
else {
$hash = $match[1];
$hash = trim($hash);
$content = $comments[$hash];
return "<!--$content-->";
}
}
/**

* Shortens long URLs to http://www.example.com/long/url...


*/
function _filter_url_trim($text, $length = NULL) {
static $_length;
if ($length !== NULL) {
$_length = $length;
}
// Use +3 for '...' string length.
if ($_length && strlen($text) > $_length + 3) {
$text = substr($text, 0, $_length) . '...';
}
return $text;
}
/**
* Filter tips callback for URL filter.
*/
function _filter_url_tips($filter, $format, $long = FALSE) {
return t('Web page addresses and e-mail addresses turn into links automaticall
y.');
}
/**
* Scan input and make sure that all HTML tags are properly closed and nested.
*/
function _filter_htmlcorrector($text) {
return filter_dom_serialize(filter_dom_load($text));
}
/**
* Convert line breaks into <p> and <br> in an intelligent fashion.
* Based on: http://photomatt.net/scripts/autop
*/
function _filter_autop($text) {
// All block level tags
$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)';
// Split at opening and closing PRE, SCRIPT, STYLE, OBJECT tags and comments.
// We don't apply any processing to the contents of these tags to avoid messin
g
// up code. We look for matched pairs and allow basic nesting. For example:
// "processed <pre> ignored <script> ignored </script> ignored </pre> processe
d"
$chunks = preg_split('@(<!--.*?-->|</?(?:pre|script|style|object|!--)[^>]*>)@i
', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
// Note: PHP ensures the array consists of alternating delimiters and literals
// and begins and ends with a literal (inserting NULL as required).
$ignore = FALSE;
$ignoretag = '';
$output = '';
foreach ($chunks as $i => $chunk) {
if ($i % 2) {
// Opening or closing tag?
$open = ($chunk[1] != '/' || $chunk[1] != '!');
$comment = (substr($chunk, 0, 4) == '<!--');
list($tag) = preg_split('/[ >]/', substr($chunk, 2 - $open), 2);
if (!$ignore) {

if ($open) {
$ignore = TRUE;
$ignoretag = $tag;
}
}
// Only allow a matching tag to close it.
elseif ((!$open && $ignoretag == $tag) || $comment) {
$ignore = FALSE;
$ignoretag = '';
}
}
elseif (!$ignore) {
$chunk = preg_replace('|\n*$|', '', $chunk) . "\n\n"; // just to make thin
gs a little easier, pad the end
$chunk = preg_replace('|<br />\s*<br />|', "\n\n", $chunk);
$chunk = preg_replace('!(<' . $block . '[^>]*>)!', "\n$1", $chunk); // Spa
ce things out a little
$chunk = preg_replace('!(</' . $block . '>)!', "$1\n\n", $chunk); // Space
things out a little
$chunk = preg_replace("/\n\n+/", "\n\n", $chunk); // take care of duplicat
es
$chunk = preg_replace('/^\n|\n\s*\n$/', '', $chunk);
$chunk = '<p>' . preg_replace('/\n\s*\n\n?(.)/', "</p>\n<p>$1", $chunk) .
"</p>\n"; // make paragraphs, including one at the end
$chunk = preg_replace("|<p>(<li.+?)</p>|", "$1", $chunk); // problem with
nested lists
$chunk = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $c
hunk);
$chunk = str_replace('</blockquote></p>', '</p></blockquote>', $chunk);
$chunk = preg_replace('|<p>\s*</p>\n?|', '', $chunk); // under certain str
ange conditions it could create a P of entirely whitespace
$chunk = preg_replace('!<p>\s*(</?' . $block . '[^>]*>)!', "$1", $chunk);
$chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*</p>!', "$1", $chunk);
$chunk = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $chunk); // make l
ine breaks
$chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*<br />!', "$1", $chunk
);
$chunk = preg_replace('!<br />(\s*</?(?:p|li|div|th|pre|td|ul|ol)>)!', '$1
', $chunk);
$chunk = preg_replace('/&([^#])(?![A-Za-z0-9]{1,8};)/', '&amp;$1', $chunk)
;
}
$output .= $chunk;
}
return $output;
}
/**
* Filter tips callback for auto-paragraph filter.
*/
function _filter_autop_tips($filter, $format, $long = FALSE) {
if ($long) {
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 insert
ed automatically. If paragraphs are not recognized simply add a couple blank lin
es.');
}
else {
return t('Lines and paragraphs break automatically.');
}

}
/**
* Escapes all HTML tags, so they will be visible instead of being effective.
*/
function _filter_html_escape($text) {
return trim(check_plain($text));
}
/**
* Filter tips callback for HTML escaping filter.
*/
function _filter_html_escape_tips($filter, $format, $long = FALSE) {
return t('No HTML tags allowed.');
}
/**
* @} End of "Standard filters".
*/

Você também pode gostar