diff --git a/database/database.mysql b/database/database.mysql index b630c173465337d78365e24df488730a3cbc2fa8..2e91654f2ca5adf2f8e40c565811756183ed52ec 100644 --- a/database/database.mysql +++ b/database/database.mysql @@ -135,7 +135,6 @@ CREATE TABLE book ( nid int(10) unsigned NOT NULL default '0', parent int(10) NOT NULL default '0', weight tinyint(3) NOT NULL default '0', - format tinyint(2) default '0', log longtext, PRIMARY KEY (nid), KEY parent (parent) @@ -150,7 +149,7 @@ CREATE TABLE boxes ( title varchar(64) NOT NULL default '', body longtext, info varchar(128) NOT NULL default '', - type tinyint(2) NOT NULL default '0', + format int(4) NOT NULL default '0', PRIMARY KEY (bid), UNIQUE KEY title (title), UNIQUE KEY info (info) @@ -184,6 +183,7 @@ CREATE TABLE comments ( timestamp int(11) NOT NULL default '0', score mediumint(9) NOT NULL default '0', status tinyint(3) unsigned NOT NULL default '0', + format int(4) NOT NULL default '0', thread varchar(255) NOT NULL, users longtext, name varchar(60) default NULL, @@ -207,14 +207,28 @@ CREATE TABLE directory ( PRIMARY KEY (link) ) TYPE=MyISAM; +-- +-- Table structure for table 'filter_formats' +-- + +CREATE TABLE filter_formats ( + format int(4) NOT NULL default '0' auto_increment, + name varchar(255) NOT NULL default '', + roles varchar(255) NOT NULL default '', + cache tinyint(2) NOT NULL default '0', + PRIMARY KEY format (format) +) TYPE=MyISAM; + -- -- Table structure for table 'filters' -- CREATE TABLE filters ( + format int(4) NOT NULL default '0', module varchar(64) NOT NULL default '', + delta tinyint(2) DEFAULT '0' NOT NULL, weight tinyint(2) DEFAULT '0' NOT NULL, - KEY module (module) + INDEX (weight) ) TYPE=MyISAM; -- @@ -330,6 +344,7 @@ CREATE TABLE node ( body longtext NOT NULL, revisions longtext NOT NULL, sticky int(2) NOT NULL default '0', + format int(4) NOT NULL default '0', PRIMARY KEY (nid), KEY node_type (type(4)), KEY node_title_type (title,type(4)), @@ -362,7 +377,6 @@ CREATE TABLE node_access ( CREATE TABLE page ( nid int(10) unsigned NOT NULL default '0', link varchar(128) NOT NULL default '', - format tinyint(2) NOT NULL default '0', description varchar(128) NOT NULL default '', PRIMARY KEY (nid) ) TYPE=MyISAM; @@ -700,3 +714,12 @@ REPLACE blocks SET module = 'user', delta = '1', status = '1'; INSERT INTO sequences (name, id) VALUES ('menu_mid', 1); INSERT INTO node_access VALUES (0, 0, 'all', 1, 0, 0); + +INSERT INTO filter_formats VALUES (1,'Filtered HTML',',1,2,',1); +INSERT INTO filter_formats VALUES (2,'PHP code','',0); +INSERT INTO filter_formats VALUES (3,'Full HTML','',1); +INSERT INTO filters VALUES (1,'filter',0,0); +INSERT INTO filters VALUES (1,'filter',3,1); +INSERT INTO filters VALUES (2,'filter',1,0); +INSERT INTO filters VALUES (3,'filter',3,0); +INSERT INTO variable (name,value) VALUES ('filter_html_1','i:1;'); diff --git a/database/database.pgsql b/database/database.pgsql index 289a2e2752409b42a83a97dc6dc34437f8e9aff5..669889f7f804bcbe4bbc52acce1950e3b4cf4499 100644 --- a/database/database.pgsql +++ b/database/database.pgsql @@ -694,6 +694,16 @@ INSERT INTO permission VALUES (2,'access comments, access content, post comments INSERT INTO blocks(module,delta,status) VALUES('user', '0', '1'); INSERT INTO blocks(module,delta,status) VALUES('user', '1', '1'); +INSERT INTO filter_formats VALUES (1,'Filtered HTML',',1,2,',1); +INSERT INTO filter_formats VALUES (2,'PHP code','',0); +INSERT INTO filter_formats VALUES (3,'Full HTML','',1)); +INSERT INTO filters VALUES (1,'filter',0,0); +INSERT INTO filters VALUES (1,'filter',3,1); +INSERT INTO filters VALUES (2,'filter',1,0); +INSERT INTO filters VALUES (3,'filter',3,0); +INSERT INTO variable (name,value) VALUES ('filter_html_1','i:1;'); + + --- --- Functions --- diff --git a/database/updates.inc b/database/updates.inc index e2faae044e75ba90137e371694b0b61a0ac33981..be5a59b810c9a70664b7ea4208693bb3de8951f7 100644 --- a/database/updates.inc +++ b/database/updates.inc @@ -69,7 +69,8 @@ "2004-07-30" => "update_95", "2004-08-04" => "update_96", "2004-08-06" => "update_97", - "2004-08-07" => "update_98" + "2004-08-07" => "update_98", + "2004-08-09" => "update_99" ); function update_32() { @@ -1308,6 +1309,159 @@ function update_98() { return array(); } +function update_99() { + // Filter patch - Multiple input formats + $ret = array(); + + /* + ** Load the list of PHP book and page nodes. + */ + $php_nodes = array(); + $res = db_query("SELECT nid FROM {book} WHERE format = 1"); + while ($book = db_fetch_object($res)) { + $php_nodes[] = $book->nid; + } + $res = db_query("SELECT nid FROM {page} WHERE format = 1"); + while ($page = db_fetch_object($res)) { + $php_nodes[] = $page->nid; + } + + /* + ** Apply database changes + */ + if ($GLOBALS['db_type'] == 'mysql') { + // Filters table + $ret[] = update_sql("ALTER TABLE {filters} ADD format int(4) NOT NULL default '0'"); + $ret[] = update_sql("ALTER TABLE {filters} ADD delta tinyint(2) NOT NULL default '0'"); + + // Filter_formats table + $ret[] = update_sql("CREATE TABLE {filter_formats} ( + format int(4) NOT NULL auto_increment, + name varchar(255) NOT NULL default '', + roles varchar(255) NOT NULL default '', + cache tinyint(2) NOT NULL default '1', + PRIMARY KEY (format) + )"); + + // Store formats in nodes, comments and boxes + $ret[] = update_sql("ALTER TABLE {boxes} CHANGE type format int(4) NOT NULL default '0'"); + $ret[] = update_sql("ALTER TABLE {comments} ADD format int(4) NOT NULL default '0'"); + $ret[] = update_sql("ALTER TABLE {node} ADD format int(4) NOT NULL default '0'"); + + // Get rid of the old book/page type info + $ret[] = update_sql("ALTER TABLE {book} DROP format"); + $ret[] = update_sql("ALTER TABLE {page} DROP format"); + } + else if ($GLOBALS['db_type'] == 'pgsql') { + // TODO: add pgsql equivalent. Whoever does this should pay attention that + // the keys/indices for the 'filters' table are correct. There was some + // inconsistency between the MySQL and PGSQL version before. + } + + // Initialize all nodes and comments to the legacy format (see below) + $ret[] = update_sql("UPDATE {node} SET format = 1"); + $ret[] = update_sql("UPDATE {comments} SET format = 1"); + + // Set format to PHP for the old PHP book/page nodes. + if (count($php_nodes)) { + $ret[] = update_sql("UPDATE {node} SET format = 2 WHERE nid IN (". implode(',', $php_nodes) .")"); + } + + // Boxes now use the filtering system as well. + // Type 0 (HTML) maps to Format 3 (Full HTML). + // Type 1 (PHP) maps to Format 2 (PHP). + $ret[] = update_sql("UPDATE {boxes} SET format = 3 - format"); + + /* + ** Update PHP content to use <?php ?> tags. + */ + if ($GLOBALS['db_type'] == 'mysql') { + $ret[] = update_sql("UPDATE {node} SET teaser = CONCAT('<?php ', teaser) WHERE format = 2"); + $ret[] = update_sql("UPDATE {node} SET body = CONCAT('<?php ', body) WHERE format = 2"); + $ret[] = update_sql("UPDATE {boxes} SET body = CONCAT('<?php ', body) WHERE format = 2"); + } + else if ($GLOBALS['db_type'] == 'pgsql') { + // TODO: someone needs to verify if this works. + $ret[] = update_sql("UPDATE {node} SET teaser = '<?php ' || teaser WHERE format = 2"); + $ret[] = update_sql("UPDATE {node} SET body = '<?php ' || body WHERE format = 2"); + $ret[] = update_sql("UPDATE {boxes} SET body = '<?php ' || body WHERE format = 2"); + } + + + /* + ** We now set up some input formats. One of these is a 'legacy' format which + ** tries to preserve as much settings as possible from before the patch. + ** The other two are 'PHP code' and 'Full HTML'. + */ + + // We pick an appropriate name for the legacy format. + $old_html_filter = variable_get('filter_html', 0); + if ($old_html_filter == FILTER_HTML_ESCAPE) { + $default = 'Plain text'; + } + else { + $default = 'Filtered HTML'; + } + // Make sure the legacy format is accessible to all roles + $all_roles = array_keys(user_roles()); + $ret[] = update_sql("INSERT INTO {filter_formats} VALUES (1,'$default',',". implode(',', $all_roles) .",',1)"); + + // Determine which roles have the old 'create php content' permission. + $res = db_query("SELECT rid FROM {permission} WHERE perm LIKE '%create php content%'"); + $php_roles = array(); + while ($role = db_fetch_object($res)) { + $php_roles[] = $role->rid; + } + $ret[] = update_sql("INSERT INTO {filter_formats} VALUES (2,'PHP code','". implode(',', $php_roles) .",',0)"); + + // This is a 'Full HTML' format which allows all HTML without restrictions. + $ret[] = update_sql("INSERT INTO {filter_formats} VALUES (3,'Full HTML','',1)"); + + // Set the default format to the legacy format + variable_set('filter_default_format', 1); + + // Put the old filters into the legacy format + $ret[] = update_sql("UPDATE {filters} SET format = 1"); + + // Add filter.module's standard filters (these used to be hardcoded). + if (!variable_get('rewrite_old_urls', 0)) { + $ret[] = update_sql("DELETE FROM {filters} WHERE module = 'filter'"); + } + else { + $ret[] = update_sql("UPDATE {filters} SET delta = 2 WHERE module ='filter'"); + } + if ($old_html_filter != 0) { + $ret[] = update_sql("INSERT INTO {filters} (format, module, delta, weight) VALUES (1,'filter',0,0)"); // HTML tag/style filter + } + $ret[] = update_sql("INSERT INTO {filters} (format, module, delta, weight) VALUES (1,'filter',3,1)"); // Linebreak filter + $ret[] = update_sql("INSERT INTO {filters} (format, module, delta, weight) VALUES (2,'filter',1,0)"); // PHP evaluator + $ret[] = update_sql("INSERT INTO {filters} (format, module, delta, weight) VALUES (3,'filter',3,0)"); // Linebreak filter + + // Migrate the settings for all core/contrib filtering modules into the legacy + // format. + $migrate = array('filter_html', // filter.module + 'allowed_html', + 'filter_style', + 'anyfilter_regexps', // anyfilter.module + 'htmlcorrector_smartclose', // htmlcorrector.module + 'htmlcorrector_xhtmlify', + 'htmlcorrector_valueentities', + 'project_filter', // project.module + 'latex_filter_link' // latex.module + ); + + foreach ($migrate as $variable) { + $value = variable_get($variable, NULL); + if ($value != NULL) { + variable_set($variable .'_1', $value); + variable_del($variable); + } + } + + return $ret; +} + + function update_sql($sql) { $edit = $_POST["edit"]; $result = db_query($sql); diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 8241fc41ffc57ea031c8b4d1b74a540f6fe704a3..0d1fb156219f8b6acb5f1a637e6b451cb04ffb1b 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -133,13 +133,22 @@ function cache_set($cid, $data, $expire = 0, $headers = NULL) { * @param $cid * If set, the cache ID to delete. Otherwise, all cache entries that can expire * are deleted. + * + * @param $wildcard + * If set to true, the $cid is treated as a substring to match rather than a + * complete ID. */ -function cache_clear_all($cid = NULL) { +function cache_clear_all($cid = NULL, $wildcard = false) { if (empty($cid)) { db_query("DELETE FROM {cache} WHERE expire <> 0"); } else { - db_query("DELETE FROM {cache} WHERE cid = '%s'", $cid); + if ($wildcard) { + db_query("DELETE FROM {cache} WHERE cid LIKE '%%%s%%'", $cid); + } + else { + db_query("DELETE FROM {cache} WHERE cid = '%s'", $cid); + } } } diff --git a/includes/common.inc b/includes/common.inc index feab010d24ebf9a50b5a2e6d6f72520c4e755cf7..209769c8fd42b96d62eff6e19c5691168207e203 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -813,7 +813,7 @@ function format_rss_item($title, $link, $description, $args = array()) { $output = "<item>\n"; $output .= ' <title>'. drupal_specialchars(strip_tags($title)) ."</title>\n"; $output .= ' <link>'. drupal_specialchars(strip_tags($link)) ."</link>\n"; - $output .= ' <description>'. drupal_specialchars(check_output($description)) ."</description>\n"; + $output .= ' <description>'. drupal_specialchars($description) ."</description>\n"; foreach ($args as $key => $value) { $output .= ' <'. $key .'>'. drupal_specialchars(strip_tags($value)) ."</$key>\n"; } @@ -1770,6 +1770,20 @@ function truncate_utf8($string, $len) { return substr($string, 0, $len); } +/** + * Wrapper around PHP's eval(). Uses output buffering to capture both returned + * and printed text. Unlike eval(), we require code to be surrounded by <?php ?> + * tags (in other words, we evaluate the code as if it were a stand-alone PHP + * file). + * + * @param $code The code to evaluate + */ +function drupal_eval($code) { + ob_start(); + print eval('?>'. $code); + return ob_get_clean(); +} + include_once 'includes/theme.inc'; include_once 'includes/pager.inc'; include_once 'includes/menu.inc'; diff --git a/misc/drupal.css b/misc/drupal.css index 4ca9f0c359ea8057068d8187d09fa45d580952da..548b122bbd9c12ee2fceeac7ae038aa746d3d973 100644 --- a/misc/drupal.css +++ b/misc/drupal.css @@ -319,6 +319,13 @@ tr.light .form-item, tr.dark .form-item { margin: 0; font-size: 0.8em; } +.tips { + margin-top: 0px; + margin-bottom: 0px; + padding-top: 0px; + padding-bottom: 0px; + font-size: 0.9em; +} #forum .description { font-size: 0.9em; margin: 0.5em; diff --git a/modules/aggregator.module b/modules/aggregator.module index 6a2d53540603ea2808326e8856574540aa80c28b..7d68ddccbaee64ce4ec3f09c41c56fdd67752f3e 100644 --- a/modules/aggregator.module +++ b/modules/aggregator.module @@ -470,7 +470,12 @@ function aggregator_parse_feed(&$data, $feed) { // Prepare the item: foreach ($item as $key => $value) { - $item[$key] = filter_default(strtr(trim($value), $tt)); + // TODO: Make handling of aggregated HTML more flexible/configurable. + $value = strtr(trim($value), $tt); + $value = strip_tags($value, '<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>'); + $value = preg_replace('/\Wstyle\s*=[^>]+?>/i', '>', $value); + $value = preg_replace('/\Won[a-z]+\s*=[^>]+?>/i', '>', $value); + $item[$key] = $value; } /* diff --git a/modules/aggregator/aggregator.module b/modules/aggregator/aggregator.module index 6a2d53540603ea2808326e8856574540aa80c28b..7d68ddccbaee64ce4ec3f09c41c56fdd67752f3e 100644 --- a/modules/aggregator/aggregator.module +++ b/modules/aggregator/aggregator.module @@ -470,7 +470,12 @@ function aggregator_parse_feed(&$data, $feed) { // Prepare the item: foreach ($item as $key => $value) { - $item[$key] = filter_default(strtr(trim($value), $tt)); + // TODO: Make handling of aggregated HTML more flexible/configurable. + $value = strtr(trim($value), $tt); + $value = strip_tags($value, '<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>'); + $value = preg_replace('/\Wstyle\s*=[^>]+?>/i', '>', $value); + $value = preg_replace('/\Won[a-z]+\s*=[^>]+?>/i', '>', $value); + $item[$key] = $value; } /* diff --git a/modules/block.module b/modules/block.module index d6aced3ca087810b349799d0b3e0b3c2b705e9b9..564f4225b7066366da5881513fabe7eddb2f0bd0 100644 --- a/modules/block.module +++ b/modules/block.module @@ -8,7 +8,7 @@ function block_help($section) { switch ($section) { case 'admin/help#block': return t(" -<p>Blocks are the boxes visible in the sidebar(s) of your web site. These are usually generated automatically by modules (e.g. recent forum topics), but you can also create your own blocks using either static HTML or dynamic PHP content.</p> +<p>Blocks are the boxes visible in the sidebar(s) of your web site. These are usually generated automatically by modules (e.g. recent forum topics), but you can also create your own blocks.</p> <p>The sidebar each block appears in depends on both which theme you're using (some are left-only, some right, some both), and on the settings in block management.</p><p>Whether a block is visible in the first place depends on four things:</p><ul><li>It must have its \"enabled\" box checked in block management.</li><li>If it has its \"custom\" box checked in block management, the user must have chosen to display it in their user preferences.</li><li>If the \"path\" field in block management is set, the visitor must be on a page that matches the path specification (more on this later).</li><li>If the block has its throttle box checked, the user will only see the block if the site throttle level is low.</li></ul> <p>The block management screen also lets you specify the vertical sort-order of the blocks within a sidebar. You do this by assigning a <strong>weight</strong> to each block. Lighter blocks (smaller weight) \"float up\" towards the top of the sidebar. Heavier ones \"sink down\" towards the bottom of it.</p> <p>The path setting lets you define the pages on which a specific block is visible. If you leave the path blank it will appear on all pages. The path uses a regular expression syntax so remember to escape special characters! The path expression is matched against the relative URL of a Drupal page, e.g. <code>book</code>, <code>node/12</code>, <code>admin</code>.</p> @@ -16,26 +16,7 @@ function block_help($section) { <p>However, for basic tasks it is sufficient to look at the following examples:</p> <p>If the block should only show up on blog pages, use <^blog>. To display on all node views use <^node>. The angular brackets are used as delimiters of the regular expression. To show up on either forum or book pages use <^(forum|book)>. The round brackets form a group of expressions, divided by the | character. It matches if any of the expressions in it match. A more complicated example is <^node/add/(story|blog|image)>. Blocks which have their paths set to this expression will show up on story, block, or image composition pages. If you want to show a block an all pages, but not the search page, use <^(?!search)>.</p> <h3>Administrator Defined Blocks</h3> -<p>An administrator defined block contains HTML, text or PHP content supplied by you (as opposed to being generated automatically by a module). Each admin-defined block consists of a title, a description, and a body containing text, HTML, or PHP code which can be as long as you wish. The Drupal engine will 'render' the content of the block.</p> -<h4>PHP in admin-defined blocks</h4> -<p>If you know how to script in PHP, Drupal gives you the power to embed any script you like inside a block. 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 don't write good code. If you are not familiar with PHP, SQL or with the site engine, avoid experimenting with PHP blocks because you can corrupt your database or render your site insecure or even unusable! If you don't plan to do fancy stuff with your blocks then you're probably better off with straight HTML.</p> -<p>Remember that the code within each PHP block must be valid PHP code - including things like correctly terminating statements with a semicolon so that the parser won't die. It is highly recommended that you develop your blocks separately using a simple test script on top of a test database before migrating to your production environment.</p> -<p>Notes:</p><ul><li>You can use global variables, such as configuration parameters, within the scope of a PHP box but remember that variables which have been given values in a PHP box will retain these values in the engine or module 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 should use the <code>return</code> statement to return the actual content for your block.</li></ul> -<p>A basic example:</p> -<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> -<pre> - return t(\"Welcome visitor, ... welcome message goes here ...\"); -</pre> -<p>If we are however dealing with a registered user, we can customize the message by using:</p> -<pre> - if (\$user->uid) { - return t(\"Welcome \$user->name, ... welcome message goes here ...\"); - } - else { - return t(\"Welcome visitor, ... welcome message goes here ...\"); - } -</pre></blockquote> -<p>For more in-depth examples, we recommend that you check the existing boxes and use them as a starting point.</p>", array('%pcre' => 'http://php.net/pcre/')); +<p>An administrator defined block contains content supplied by you (as opposed to being generated automatically by a module). Each admin-defined block consists of a title, a description, and a body which can be as long as you wish. The Drupal engine will 'render' the content of the block.</p>", array('%pcre' => 'http://php.net/pcre/')); case 'admin/modules#description': return t('Controls the boxes that are displayed around the main content.'); case 'admin/block': @@ -101,7 +82,7 @@ function block_block($op = 'list', $delta = 0) { else { $block = db_fetch_object(db_query('SELECT * FROM {boxes} WHERE bid = %d', $delta)); $data['subject'] = $block->title; - $data['content'] = ($block->type == 1) ? eval($block->body) : $block->body; + $data['content'] = check_output($block->body, $block->format); return $data; } } @@ -179,11 +160,20 @@ function block_admin_display() { $blocks = _block_rehash(); + // Fetch input formats used by admin-defined boxes. + $formats = array(); + $result = db_query('SELECT bid, format FROM {boxes}'); + while ($box = db_fetch_object($result)) { + $formats[$box->bid] = $box->format; + } + $header = array(t('block'), t('enabled'), t('custom'), t('throttle'), t('weight'), t('region'), t('path'), array('data' => t('operations'), 'colspan' => 2)); foreach ($blocks as $block) { if ($block['module'] == 'block') { - $edit = l(t('edit'), 'admin/block/edit/'. $block['delta']); + if (filter_access($formats[$block['delta']])) { + $edit = l(t('edit'), 'admin/block/edit/'. $block['delta']); + } $delete = l(t('delete'), 'admin/block/delete/'. $block['delta']); } else { @@ -232,14 +222,10 @@ function block_box_edit($bid = 0) { } function block_box_form($edit = array()) { - $type = array(0 => 'HTML', 1 => 'PHP'); - $group = form_textfield(t('Block title'), 'title', $edit['title'], 50, 64, t('The title of the block as shown to the user.')); + $group .= filter_form('format', $edit['format']); $group .= form_textarea(t('Block body'), 'body', $edit['body'], 70, 10, t('The content of the block as shown to the user.')); $group .= form_textfield(t('Block description'), 'info', $edit['info'], 50, 64, t('A brief description of your block. Used on the <a href="%overview">block overview page</a>.', array('%overview' => url('admin/block')))); - if (user_access('create php content')) { - $group .= form_radios(t('Block type'), 'type', $edit['type'], $type, t("If you would like to use PHP code inside your block, set the above option to 'PHP' instead of 'HTML'.")); - } if ($edit['bid']) { $group .= form_hidden('bid', $edit['bid']); @@ -252,16 +238,16 @@ function block_box_form($edit = array()) { } function block_box_save($edit) { - if (!user_access('create php content')) { - $edit['type'] = 0; + if (!filter_access($edit['format'])) { + $edit['format'] = FILTER_FORMAT_DEFAULT; } if ($edit['bid']) { - db_query("UPDATE {boxes} SET title = '%s', body = '%s', info = '%s', type = %d WHERE bid = %d", $edit['title'], $edit['body'], $edit['info'], $edit['type'], $edit['bid']); + db_query("UPDATE {boxes} SET title = '%s', body = '%s', info = '%s', format = %d WHERE bid = %d", $edit['title'], $edit['body'], $edit['info'], $edit['format'], $edit['bid']); return t('the block has been updated.'); } else { - db_query("INSERT INTO {boxes} (title, body, info, type) VALUES ('%s', '%s', '%s', %d)", $edit['title'], $edit['body'], $edit['info'], $edit['type']); + db_query("INSERT INTO {boxes} (title, body, info, format) VALUES ('%s', '%s', '%s', %d)", $edit['title'], $edit['body'], $edit['info'], $edit['format']); return t('the new block has been added.'); } } diff --git a/modules/block/block.module b/modules/block/block.module index d6aced3ca087810b349799d0b3e0b3c2b705e9b9..564f4225b7066366da5881513fabe7eddb2f0bd0 100644 --- a/modules/block/block.module +++ b/modules/block/block.module @@ -8,7 +8,7 @@ function block_help($section) { switch ($section) { case 'admin/help#block': return t(" -<p>Blocks are the boxes visible in the sidebar(s) of your web site. These are usually generated automatically by modules (e.g. recent forum topics), but you can also create your own blocks using either static HTML or dynamic PHP content.</p> +<p>Blocks are the boxes visible in the sidebar(s) of your web site. These are usually generated automatically by modules (e.g. recent forum topics), but you can also create your own blocks.</p> <p>The sidebar each block appears in depends on both which theme you're using (some are left-only, some right, some both), and on the settings in block management.</p><p>Whether a block is visible in the first place depends on four things:</p><ul><li>It must have its \"enabled\" box checked in block management.</li><li>If it has its \"custom\" box checked in block management, the user must have chosen to display it in their user preferences.</li><li>If the \"path\" field in block management is set, the visitor must be on a page that matches the path specification (more on this later).</li><li>If the block has its throttle box checked, the user will only see the block if the site throttle level is low.</li></ul> <p>The block management screen also lets you specify the vertical sort-order of the blocks within a sidebar. You do this by assigning a <strong>weight</strong> to each block. Lighter blocks (smaller weight) \"float up\" towards the top of the sidebar. Heavier ones \"sink down\" towards the bottom of it.</p> <p>The path setting lets you define the pages on which a specific block is visible. If you leave the path blank it will appear on all pages. The path uses a regular expression syntax so remember to escape special characters! The path expression is matched against the relative URL of a Drupal page, e.g. <code>book</code>, <code>node/12</code>, <code>admin</code>.</p> @@ -16,26 +16,7 @@ function block_help($section) { <p>However, for basic tasks it is sufficient to look at the following examples:</p> <p>If the block should only show up on blog pages, use <^blog>. To display on all node views use <^node>. The angular brackets are used as delimiters of the regular expression. To show up on either forum or book pages use <^(forum|book)>. The round brackets form a group of expressions, divided by the | character. It matches if any of the expressions in it match. A more complicated example is <^node/add/(story|blog|image)>. Blocks which have their paths set to this expression will show up on story, block, or image composition pages. If you want to show a block an all pages, but not the search page, use <^(?!search)>.</p> <h3>Administrator Defined Blocks</h3> -<p>An administrator defined block contains HTML, text or PHP content supplied by you (as opposed to being generated automatically by a module). Each admin-defined block consists of a title, a description, and a body containing text, HTML, or PHP code which can be as long as you wish. The Drupal engine will 'render' the content of the block.</p> -<h4>PHP in admin-defined blocks</h4> -<p>If you know how to script in PHP, Drupal gives you the power to embed any script you like inside a block. 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 don't write good code. If you are not familiar with PHP, SQL or with the site engine, avoid experimenting with PHP blocks because you can corrupt your database or render your site insecure or even unusable! If you don't plan to do fancy stuff with your blocks then you're probably better off with straight HTML.</p> -<p>Remember that the code within each PHP block must be valid PHP code - including things like correctly terminating statements with a semicolon so that the parser won't die. It is highly recommended that you develop your blocks separately using a simple test script on top of a test database before migrating to your production environment.</p> -<p>Notes:</p><ul><li>You can use global variables, such as configuration parameters, within the scope of a PHP box but remember that variables which have been given values in a PHP box will retain these values in the engine or module 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 should use the <code>return</code> statement to return the actual content for your block.</li></ul> -<p>A basic example:</p> -<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> -<pre> - return t(\"Welcome visitor, ... welcome message goes here ...\"); -</pre> -<p>If we are however dealing with a registered user, we can customize the message by using:</p> -<pre> - if (\$user->uid) { - return t(\"Welcome \$user->name, ... welcome message goes here ...\"); - } - else { - return t(\"Welcome visitor, ... welcome message goes here ...\"); - } -</pre></blockquote> -<p>For more in-depth examples, we recommend that you check the existing boxes and use them as a starting point.</p>", array('%pcre' => 'http://php.net/pcre/')); +<p>An administrator defined block contains content supplied by you (as opposed to being generated automatically by a module). Each admin-defined block consists of a title, a description, and a body which can be as long as you wish. The Drupal engine will 'render' the content of the block.</p>", array('%pcre' => 'http://php.net/pcre/')); case 'admin/modules#description': return t('Controls the boxes that are displayed around the main content.'); case 'admin/block': @@ -101,7 +82,7 @@ function block_block($op = 'list', $delta = 0) { else { $block = db_fetch_object(db_query('SELECT * FROM {boxes} WHERE bid = %d', $delta)); $data['subject'] = $block->title; - $data['content'] = ($block->type == 1) ? eval($block->body) : $block->body; + $data['content'] = check_output($block->body, $block->format); return $data; } } @@ -179,11 +160,20 @@ function block_admin_display() { $blocks = _block_rehash(); + // Fetch input formats used by admin-defined boxes. + $formats = array(); + $result = db_query('SELECT bid, format FROM {boxes}'); + while ($box = db_fetch_object($result)) { + $formats[$box->bid] = $box->format; + } + $header = array(t('block'), t('enabled'), t('custom'), t('throttle'), t('weight'), t('region'), t('path'), array('data' => t('operations'), 'colspan' => 2)); foreach ($blocks as $block) { if ($block['module'] == 'block') { - $edit = l(t('edit'), 'admin/block/edit/'. $block['delta']); + if (filter_access($formats[$block['delta']])) { + $edit = l(t('edit'), 'admin/block/edit/'. $block['delta']); + } $delete = l(t('delete'), 'admin/block/delete/'. $block['delta']); } else { @@ -232,14 +222,10 @@ function block_box_edit($bid = 0) { } function block_box_form($edit = array()) { - $type = array(0 => 'HTML', 1 => 'PHP'); - $group = form_textfield(t('Block title'), 'title', $edit['title'], 50, 64, t('The title of the block as shown to the user.')); + $group .= filter_form('format', $edit['format']); $group .= form_textarea(t('Block body'), 'body', $edit['body'], 70, 10, t('The content of the block as shown to the user.')); $group .= form_textfield(t('Block description'), 'info', $edit['info'], 50, 64, t('A brief description of your block. Used on the <a href="%overview">block overview page</a>.', array('%overview' => url('admin/block')))); - if (user_access('create php content')) { - $group .= form_radios(t('Block type'), 'type', $edit['type'], $type, t("If you would like to use PHP code inside your block, set the above option to 'PHP' instead of 'HTML'.")); - } if ($edit['bid']) { $group .= form_hidden('bid', $edit['bid']); @@ -252,16 +238,16 @@ function block_box_form($edit = array()) { } function block_box_save($edit) { - if (!user_access('create php content')) { - $edit['type'] = 0; + if (!filter_access($edit['format'])) { + $edit['format'] = FILTER_FORMAT_DEFAULT; } if ($edit['bid']) { - db_query("UPDATE {boxes} SET title = '%s', body = '%s', info = '%s', type = %d WHERE bid = %d", $edit['title'], $edit['body'], $edit['info'], $edit['type'], $edit['bid']); + db_query("UPDATE {boxes} SET title = '%s', body = '%s', info = '%s', format = %d WHERE bid = %d", $edit['title'], $edit['body'], $edit['info'], $edit['format'], $edit['bid']); return t('the block has been updated.'); } else { - db_query("INSERT INTO {boxes} (title, body, info, type) VALUES ('%s', '%s', '%s', %d)", $edit['title'], $edit['body'], $edit['info'], $edit['type']); + db_query("INSERT INTO {boxes} (title, body, info, format) VALUES ('%s', '%s', '%s', %d)", $edit['title'], $edit['body'], $edit['info'], $edit['format']); return t('the new block has been added.'); } } diff --git a/modules/blog.module b/modules/blog.module index 0401a98fc2d3976d3da7fbe1f2a67bd9e4c27c30..a9343c7fc01f1b6d1d571341ac9af9ccff598f43 100644 --- a/modules/blog.module +++ b/modules/blog.module @@ -197,7 +197,7 @@ function blog_form(&$node) { if ($iid && $item = db_fetch_object(db_query('SELECT i.*, f.title as ftitle, f.link as flink FROM {aggregator_item} i, {aggregator_feed} f WHERE i.iid = %d AND i.fid = f.fid', $iid))) { $node->title = $item->title; - $node->body = "<a href=\"$item->link\">$item->title</a> - <i>". check_output($item->description) ."</i> [<a href=\"$item->flink\">$item->ftitle</a>]\n"; + $node->body = "<a href=\"$item->link\">$item->title</a> - <i>". $item->description ."</i> [<a href=\"$item->flink\">$item->ftitle</a>]\n"; } } @@ -205,7 +205,7 @@ function blog_form(&$node) { $output .= implode('', taxonomy_node_form('blog', $node)); } - $output .= form_textarea(t('Body'), 'body', $node->body, 60, 15, filter_tips_short(), NULL, TRUE); + $output .= form_textarea(t('Body'), 'body', $node->body, 60, 15, '', NULL, TRUE); return $output; } diff --git a/modules/blog/blog.module b/modules/blog/blog.module index 0401a98fc2d3976d3da7fbe1f2a67bd9e4c27c30..a9343c7fc01f1b6d1d571341ac9af9ccff598f43 100644 --- a/modules/blog/blog.module +++ b/modules/blog/blog.module @@ -197,7 +197,7 @@ function blog_form(&$node) { if ($iid && $item = db_fetch_object(db_query('SELECT i.*, f.title as ftitle, f.link as flink FROM {aggregator_item} i, {aggregator_feed} f WHERE i.iid = %d AND i.fid = f.fid', $iid))) { $node->title = $item->title; - $node->body = "<a href=\"$item->link\">$item->title</a> - <i>". check_output($item->description) ."</i> [<a href=\"$item->flink\">$item->ftitle</a>]\n"; + $node->body = "<a href=\"$item->link\">$item->title</a> - <i>". $item->description ."</i> [<a href=\"$item->flink\">$item->ftitle</a>]\n"; } } @@ -205,7 +205,7 @@ function blog_form(&$node) { $output .= implode('', taxonomy_node_form('blog', $node)); } - $output .= form_textarea(t('Body'), 'body', $node->body, 60, 15, filter_tips_short(), NULL, TRUE); + $output .= form_textarea(t('Body'), 'body', $node->body, 60, 15, '', NULL, TRUE); return $output; } diff --git a/modules/blogapi.module b/modules/blogapi.module index 41f7eebdea089a344b34fd87a4f20c761768bc08..8a380149538b3ee2c7d0d203c9ec1670fb43d092 100644 --- a/modules/blogapi.module +++ b/modules/blogapi.module @@ -130,6 +130,7 @@ function blogapi_new_post($req_params) { 'promote' => $promote, 'comment' => $comment, 'moderate' => $moderate, + 'format' => FILTER_DEFAULT_FORMAT, 'revision' => $revision )); diff --git a/modules/blogapi/blogapi.module b/modules/blogapi/blogapi.module index 41f7eebdea089a344b34fd87a4f20c761768bc08..8a380149538b3ee2c7d0d203c9ec1670fb43d092 100644 --- a/modules/blogapi/blogapi.module +++ b/modules/blogapi/blogapi.module @@ -130,6 +130,7 @@ function blogapi_new_post($req_params) { 'promote' => $promote, 'comment' => $comment, 'moderate' => $moderate, + 'format' => FILTER_DEFAULT_FORMAT, 'revision' => $revision )); diff --git a/modules/book.module b/modules/book.module index a56d2bc8bd76807e341db74c8e8da9bd4383bc34..a2111a00febdf6e4c016da2c4cc32d663f0b8419 100644 --- a/modules/book.module +++ b/modules/book.module @@ -39,10 +39,11 @@ function book_access($op, $node) { // Only registered users can update book pages. Given the nature // of the book module this is considered to be a good/safe idea. // One can only update a book page if there are no suggested updates - // of that page waiting for approval, it is not a PHP page, and - // the "create new revision" bit is set. That is, only updates that - // don't overwrite the current or pending information are allowed. - return user_access('maintain books') && !$node->moderate && !$node->format && $node->revision; + // of that page waiting for approval and as long as the "create new + // revision"-bit is set. That is, only updates that don't overwrite + // the current or pending information are allowed. + + return user_access('maintain books') && !$node->moderate && $node->revision; } } @@ -139,7 +140,7 @@ function book_block($op = 'list', $delta = 0) { function book_load($node) { global $user; - $book = db_fetch_object(db_query('SELECT format, parent, weight, log FROM {book} WHERE nid = %d', $node->nid)); + $book = db_fetch_object(db_query('SELECT parent, weight, log FROM {book} WHERE nid = %d', $node->nid)); if (arg(1) == 'edit' && !user_access('administer nodes')) { // If a user is about to update a book page, we overload some @@ -166,14 +167,14 @@ function book_load($node) { * Implementation of hook_insert(). */ function book_insert($node) { - db_query("INSERT INTO {book} (nid, format, parent, weight, log) VALUES (%d, %d, %d, %d, '%s')", $node->nid, $node->format, $node->parent, $node->weight, $node->log); + db_query("INSERT INTO {book} (nid, parent, weight, log) VALUES (%d, %d, %d, '%s')", $node->nid, $node->parent, $node->weight, $node->log); } /** * Implementation of hook_update(). */ function book_update($node) { - db_query("UPDATE {book} SET format = %d, parent = %d, weight = %d, log = '%s' WHERE nid = %d", $node->format, $node->parent, $node->weight, $node->log, $node->nid); + db_query("UPDATE {book} SET parent = %d, weight = %d, log = '%s' WHERE nid = %d", $node->parent, $node->weight, $node->log, $node->nid); } /** @@ -187,17 +188,8 @@ function book_delete(&$node) { * Implementation of hook_validate(). */ function book_validate(&$node) { - if ($node->format && user_access('create php content')) { - // Do not filter PHP code. Do not auto-extract a teaser. - $node->teaser = $node->body; - } - else { - $node->format = 0; - } - // Set default values for non-administrators. if (!user_access('administer nodes')) { - $node->format = 0; $node->weight = 0; $node->revision = 1; } @@ -216,14 +208,11 @@ function book_form(&$node) { $output .= implode('', taxonomy_node_form('book', $node)); } - $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, filter_tips_short(), NULL, TRUE); + $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE); $output .= form_textarea(t('Log message'), 'log', $node->log, 60, 5, t('An explanation of the additions or updates being made to help the group understand your motivations.')); if (user_access('administer nodes')) { $output .= form_weight(t('Weight'), 'weight', $node->weight, 15, t('The heavier pages will sink and the lighter pages will be positioned nearer the top.')); - if (user_access('create php content')) { - $output .= form_radios(t('Type'), 'format', $node->format, array(0 => 'HTML / text', 1 => 'PHP')); - } } else { // If a regular user updates a book page, we create a new revision @@ -401,24 +390,8 @@ function book_content($node, $teaser = FALSE) { } } - // Extract the page body. If body is dynamic (using PHP code), the body - // will be generated. - if ($node->format == 1) { - // Make sure only authorized users can preview PHP pages. - if ($op == t('Preview') && !user_access('create php content')) { - return; - } - - ob_start(); - eval($node->body); - $node->body = ob_get_contents(); - ob_end_clean(); - $node->teaser = node_teaser($node->body); - $node->readmore = (strlen($node->teaser) < strlen($node->body)); - } - else { - $node = node_prepare($node, $teaser); - } + // Extract the page body. + $node = node_prepare($node, $teaser); return $node; } diff --git a/modules/book/book.module b/modules/book/book.module index a56d2bc8bd76807e341db74c8e8da9bd4383bc34..a2111a00febdf6e4c016da2c4cc32d663f0b8419 100644 --- a/modules/book/book.module +++ b/modules/book/book.module @@ -39,10 +39,11 @@ function book_access($op, $node) { // Only registered users can update book pages. Given the nature // of the book module this is considered to be a good/safe idea. // One can only update a book page if there are no suggested updates - // of that page waiting for approval, it is not a PHP page, and - // the "create new revision" bit is set. That is, only updates that - // don't overwrite the current or pending information are allowed. - return user_access('maintain books') && !$node->moderate && !$node->format && $node->revision; + // of that page waiting for approval and as long as the "create new + // revision"-bit is set. That is, only updates that don't overwrite + // the current or pending information are allowed. + + return user_access('maintain books') && !$node->moderate && $node->revision; } } @@ -139,7 +140,7 @@ function book_block($op = 'list', $delta = 0) { function book_load($node) { global $user; - $book = db_fetch_object(db_query('SELECT format, parent, weight, log FROM {book} WHERE nid = %d', $node->nid)); + $book = db_fetch_object(db_query('SELECT parent, weight, log FROM {book} WHERE nid = %d', $node->nid)); if (arg(1) == 'edit' && !user_access('administer nodes')) { // If a user is about to update a book page, we overload some @@ -166,14 +167,14 @@ function book_load($node) { * Implementation of hook_insert(). */ function book_insert($node) { - db_query("INSERT INTO {book} (nid, format, parent, weight, log) VALUES (%d, %d, %d, %d, '%s')", $node->nid, $node->format, $node->parent, $node->weight, $node->log); + db_query("INSERT INTO {book} (nid, parent, weight, log) VALUES (%d, %d, %d, '%s')", $node->nid, $node->parent, $node->weight, $node->log); } /** * Implementation of hook_update(). */ function book_update($node) { - db_query("UPDATE {book} SET format = %d, parent = %d, weight = %d, log = '%s' WHERE nid = %d", $node->format, $node->parent, $node->weight, $node->log, $node->nid); + db_query("UPDATE {book} SET parent = %d, weight = %d, log = '%s' WHERE nid = %d", $node->parent, $node->weight, $node->log, $node->nid); } /** @@ -187,17 +188,8 @@ function book_delete(&$node) { * Implementation of hook_validate(). */ function book_validate(&$node) { - if ($node->format && user_access('create php content')) { - // Do not filter PHP code. Do not auto-extract a teaser. - $node->teaser = $node->body; - } - else { - $node->format = 0; - } - // Set default values for non-administrators. if (!user_access('administer nodes')) { - $node->format = 0; $node->weight = 0; $node->revision = 1; } @@ -216,14 +208,11 @@ function book_form(&$node) { $output .= implode('', taxonomy_node_form('book', $node)); } - $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, filter_tips_short(), NULL, TRUE); + $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE); $output .= form_textarea(t('Log message'), 'log', $node->log, 60, 5, t('An explanation of the additions or updates being made to help the group understand your motivations.')); if (user_access('administer nodes')) { $output .= form_weight(t('Weight'), 'weight', $node->weight, 15, t('The heavier pages will sink and the lighter pages will be positioned nearer the top.')); - if (user_access('create php content')) { - $output .= form_radios(t('Type'), 'format', $node->format, array(0 => 'HTML / text', 1 => 'PHP')); - } } else { // If a regular user updates a book page, we create a new revision @@ -401,24 +390,8 @@ function book_content($node, $teaser = FALSE) { } } - // Extract the page body. If body is dynamic (using PHP code), the body - // will be generated. - if ($node->format == 1) { - // Make sure only authorized users can preview PHP pages. - if ($op == t('Preview') && !user_access('create php content')) { - return; - } - - ob_start(); - eval($node->body); - $node->body = ob_get_contents(); - ob_end_clean(); - $node->teaser = node_teaser($node->body); - $node->readmore = (strlen($node->teaser) < strlen($node->body)); - } - else { - $node = node_prepare($node, $teaser); - } + // Extract the page body. + $node = node_prepare($node, $teaser); return $node; } diff --git a/modules/comment.module b/modules/comment.module index 349635bc19990b0456e62776b5351468bdff91c2..740bc1131abc77d206f8175ec30a02c59c3a2853 100644 --- a/modules/comment.module +++ b/modules/comment.module @@ -319,7 +319,7 @@ function comment_update_index() { function comment_user($type, $edit, &$user, $category = NULL) { if ($type == 'form' && $category == 'account') { // when user tries to edit his own data - return array(array('title' => t('Comment settings'), 'data' => form_textarea(t('Signature'), 'signature', $user->signature, 64, 3, t('Your signature will be publicly displayed at the end of your comments.') .'<br />'. filter_tips_short()), 'weight' => 2)); + return array(array('title' => t('Comment settings'), 'data' => form_textarea(t('Signature'), 'signature', $user->signature, 64, 3, t('Your signature will be publicly displayed at the end of your comments.')), 'weight' => 2)); } if ($type == 'validate') { // validate user data editing @@ -455,6 +455,13 @@ function comment_validate_form($edit) { form_set_error('comment', t('The body of your comment is empty.')); } + /* + ** Validate filter format + */ + if (!filter_access($edit['format'])) { + form_set_error('format', t('The supplied input format is invalid.')); + } + /* ** Check validity of name, mail and homepage (if given) */ @@ -953,7 +960,7 @@ function comment_admin_edit($cid) { if ($comment) { $form .= form_item(t('Author'), format_name($comment)); $form .= form_textfield(t('Subject'), 'subject', $comment->subject, 70, 128); - $form .= form_textarea(t('Comment'), 'comment', $comment->comment, 70, 15, filter_tips_short()); + $form .= form_textarea(t('Comment'), 'comment', $comment->comment, 70, 15, ''); $form .= form_radios(t('Status'), 'status', $comment->status, array('published', 'not published')); $form .= form_hidden('cid', $cid); $form .= form_submit(t('Submit')); @@ -986,7 +993,7 @@ function comment_delete($cid) { // Print a confirmation. else if ($comment->cid) { drupal_set_message(t('do you want to delete this comment and all its replies?')); - $comment->comment = check_output($comment->comment); + $comment->comment = check_output($comment->comment, $comment->format); $output = theme('comment', $comment); $output .= form_submit(t('Delete comment')); } @@ -1406,8 +1413,11 @@ function theme_comment_form($edit, $title) { $form .= form_textfield(t('Subject'), 'subject', $edit['subject'], 50, 64); } + // format selector + $form .= filter_form('format', $edit['format']); + // comment field: - $form .= form_textarea(t('Comment'), 'comment', $edit['comment'] ? $edit['comment'] : $user->signature, 70, 10, filter_tips_short(), NULL, TRUE); + $form .= form_textarea(t('Comment'), 'comment', $edit['comment'] ? $edit['comment'] : $user->signature, 70, 10, '', NULL, TRUE); // preview button: $form .= form_hidden('cid', $edit['cid']); @@ -1438,7 +1448,7 @@ function theme_comment_view($comment, $links = '', $visible = 1) { // Switch to folded/unfolded view of the comment if ($visible) { - $comment->comment = check_output($comment->comment); + $comment->comment = check_output($comment->comment, $comment->format); $output .= theme('comment', $comment, $links); } else { diff --git a/modules/comment/comment.module b/modules/comment/comment.module index 349635bc19990b0456e62776b5351468bdff91c2..740bc1131abc77d206f8175ec30a02c59c3a2853 100644 --- a/modules/comment/comment.module +++ b/modules/comment/comment.module @@ -319,7 +319,7 @@ function comment_update_index() { function comment_user($type, $edit, &$user, $category = NULL) { if ($type == 'form' && $category == 'account') { // when user tries to edit his own data - return array(array('title' => t('Comment settings'), 'data' => form_textarea(t('Signature'), 'signature', $user->signature, 64, 3, t('Your signature will be publicly displayed at the end of your comments.') .'<br />'. filter_tips_short()), 'weight' => 2)); + return array(array('title' => t('Comment settings'), 'data' => form_textarea(t('Signature'), 'signature', $user->signature, 64, 3, t('Your signature will be publicly displayed at the end of your comments.')), 'weight' => 2)); } if ($type == 'validate') { // validate user data editing @@ -455,6 +455,13 @@ function comment_validate_form($edit) { form_set_error('comment', t('The body of your comment is empty.')); } + /* + ** Validate filter format + */ + if (!filter_access($edit['format'])) { + form_set_error('format', t('The supplied input format is invalid.')); + } + /* ** Check validity of name, mail and homepage (if given) */ @@ -953,7 +960,7 @@ function comment_admin_edit($cid) { if ($comment) { $form .= form_item(t('Author'), format_name($comment)); $form .= form_textfield(t('Subject'), 'subject', $comment->subject, 70, 128); - $form .= form_textarea(t('Comment'), 'comment', $comment->comment, 70, 15, filter_tips_short()); + $form .= form_textarea(t('Comment'), 'comment', $comment->comment, 70, 15, ''); $form .= form_radios(t('Status'), 'status', $comment->status, array('published', 'not published')); $form .= form_hidden('cid', $cid); $form .= form_submit(t('Submit')); @@ -986,7 +993,7 @@ function comment_delete($cid) { // Print a confirmation. else if ($comment->cid) { drupal_set_message(t('do you want to delete this comment and all its replies?')); - $comment->comment = check_output($comment->comment); + $comment->comment = check_output($comment->comment, $comment->format); $output = theme('comment', $comment); $output .= form_submit(t('Delete comment')); } @@ -1406,8 +1413,11 @@ function theme_comment_form($edit, $title) { $form .= form_textfield(t('Subject'), 'subject', $edit['subject'], 50, 64); } + // format selector + $form .= filter_form('format', $edit['format']); + // comment field: - $form .= form_textarea(t('Comment'), 'comment', $edit['comment'] ? $edit['comment'] : $user->signature, 70, 10, filter_tips_short(), NULL, TRUE); + $form .= form_textarea(t('Comment'), 'comment', $edit['comment'] ? $edit['comment'] : $user->signature, 70, 10, '', NULL, TRUE); // preview button: $form .= form_hidden('cid', $edit['cid']); @@ -1438,7 +1448,7 @@ function theme_comment_view($comment, $links = '', $visible = 1) { // Switch to folded/unfolded view of the comment if ($visible) { - $comment->comment = check_output($comment->comment); + $comment->comment = check_output($comment->comment, $comment->format); $output .= theme('comment', $comment, $links); } else { diff --git a/modules/filter.module b/modules/filter.module index dbb2eab99f3f6291c6c4a1c39043d53967cfdeff..f5e0834c1c6e6371297b0a6d0201580c7471eb80 100644 --- a/modules/filter.module +++ b/modules/filter.module @@ -1,7 +1,8 @@ <?php // $Id$ -define('FILTER_HTML_DONOTHING', 0); +define('FILTER_FORMAT_DEFAULT', 0); + define('FILTER_HTML_STRIP', 1); define('FILTER_HTML_ESCAPE', 2); @@ -12,52 +13,135 @@ * Implementation of hook_help(). */ function filter_help($section) { + // Get rid of variable numbers in the URL + $section = preg_replace('/[0-9]+/', '#', $section); + switch ($section) { case 'admin/modules#description': return t('Framework for handling filtering of content.'); + case 'admin/filters': - return t(" -<p>Filters fit between the raw text in posts and comments, and the HTML output. They allow you to replace text selectively. Uses include automatic conversion of emoticons into graphics and filtering HTML content from users' submissions.</p> -<p>If you notice some filters are causing conflicts in the output, you can <a href=\"%url\">rearrange them</a>.</p>", array('%url' => url('admin/filters/order'))); - case 'admin/filters/order': - return t(" + return t(' +<p><i>Input formats</i> define a way of processing user-supplied text in Drupal. Every input format has its own settings of which <i>filters</i> to apply. Possible filters include stripping out malicious HTML and making URLs clickable.</p> +<p>Users can choose between the available input formats when submitting content.</p> +<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>'); + + case 'admin/filters/#': + return t(' +<p>Every <i>filter</i> 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> +<p>If you notice some filters are causing conflicts in the output, you can <a href="%order">rearrange them</a>.', array('%configure' => url('admin/filters/'. arg(2) .'/configure'), '%order' => url('admin/filters/'. arg(2) .'/order'))); + + case 'admin/filters/#/configure': + return t(' +<p>If you cannot find the settings for a certain filter, make sure you\'ve enabled it on the <a href="%url">list filters</a> tab first.</p>', array('%url' => url('admin/filters/'. arg(2) .'/list'))); + + case 'admin/filters/#/order': + return t(' <p>Because of the flexible filtering system, you might encounter a situation where one filter prevents another from doing its job. For example: a word in an URL gets converted into a glossary term, before the URL can be converted in a clickable link. When this happens, you will need to rearrange the order in which filters get executed.</p> -<p>Filters are executed from top-to-bottom. You can use the weight column to rearrange them: heavier filters 'sink' to the bottom. Standard HTML filtering is always run first.</p>"); - case 'filter#long-tip': - case 'filter#short-tip': - switch (variable_get('filter_html', FILTER_HTML_DONOTHING)) { - case 0: - return t('All HTML tags allowed'); - break; - case 1: - if ($allowed_html = variable_get('allowed_html', '<a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul>')) { +<p>Filters are executed from top-to-bottom. You can use the weight column to rearrange them: heavier filters \'sink\' to the bottom.</p>'); + } +} + +/** + * Implementation of hook_filter_tips. + */ +function filter_filter_tips($delta, $format, $type = false) { + switch ($delta) { + case 0: + switch (variable_get("filter_html_$format", FILTER_HTML_STRIP)) { + + case FILTER_HTML_STRIP: + if ($allowed_html = variable_get("allowed_html_$format", '<a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul>')) { return t('Allowed HTML tags') .': '. htmlspecialchars($allowed_html); - } else { + } + else { return t('No HTML tags allowed'); } - break; - case 2: + + case FILTER_STYLE_STRIP: return t('No HTML tags allowed'); - break; } break; + + case 1: + switch ($type) { + case 0: + return t('You may post PHP code. You should include <?php ?> tags.'); + case 1: + return t(' +<h4>Using custom PHP code</h4> +<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 don\'t 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 don\'t plan to do fancy stuff with your content then you\'re probably better off with straight HTML.</p> +<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> +<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> +<p>A basic example:</p> +<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> +<pre> + print t("Welcome visitor, ... welcome message goes here ..."); +</pre> +<p>If we are however dealing with a registered user, we can customize the message by using:</p> +<pre> + global $user; + if ($user->uid) { + print t("Welcome $user->name, ... welcome message goes here ..."); + } + else { + print t("Welcome visitor, ... welcome message goes here ..."); + } +</pre></blockquote> +<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>'); + } + + case 3: + return t('Lines and paragraphs break automatically.'); + break; } } + /** * Implementation of hook_menu(). */ function filter_menu() { $items = array(); - $items[] = array('path' => 'admin/filters', 'title' => t('filters'), - 'callback' => 'filter_admin_settings', - 'access' => user_access('administer site configuration')); - $items[] = array('path' => 'admin/filters/configure', 'title' => t('configure'), - 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); - $items[] = array('path' => 'admin/filters/order', 'title' => t('rearrange'), - 'callback' => 'filter_admin_order', - 'access' => user_access('administer site configuration'), - 'type' => MENU_LOCAL_TASK); + + $items[] = array('path' => 'admin/filters', 'title' => t('input formats'), + 'callback' => 'filter_admin_overview', + 'access' => user_access('administer filters')); + + $items[] = array('path' => 'admin/filters/delete', 'title' => t('delete input format'), + 'callback' => 'filter_admin_delete', + 'type' => MENU_CALLBACK, + 'access' => user_access('administer filters')); + + if (arg(0) == 'admin' && arg(1) == 'filters' && is_numeric(arg(2))) { + $formats = filter_formats(); + + if (isset($formats[arg(2)])) { + $items[] = array('path' => 'admin/filters/'. arg(2), 'title' => t("'%format' input format", array('%format' => $formats[arg(2)]->name)), + 'callback' => 'filter_admin_filters', + 'type' => MENU_CALLBACK, + 'access' => user_access('administer filters')); + + $items[] = array('path' => 'admin/filters/'. arg(2) .'/list', 'title' => t('list filters'), + 'callback' => 'filter_admin_filters', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => 0, + 'access' => user_access('administer filters')); + + $items[] = array('path' => 'admin/filters/'. arg(2) .'/configure', 'title' => t('configure filters'), + 'callback' => 'filter_admin_configure', + 'type' => MENU_LOCAL_TASK, + 'weight' => 1, + 'access' => user_access('administer filters')); + + $items[] = array('path' => 'admin/filters/'. arg(2) .'/order', 'title' => t('rearrange filters'), + 'callback' => 'filter_admin_order', + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, + 'access' => user_access('administer filters')); + } + } + $items[] = array('path' => 'filter/tips', 'title' => t('compose tips'), 'callback' => 'filter_tips_long', 'access' => TRUE, 'type' => MENU_SUGGESTED_ITEM); @@ -65,30 +149,271 @@ function filter_menu() { } /** - * Menu callback; allows administrators to change the filter ordering. + * Implementation of hook_perm() */ -function filter_admin_order() { +function filter_perm() { + return array('administer filters'); +} + +/** + * Menu callback: allows administrators to setup input formats + */ +function filter_admin_overview() { + // Process form submission + switch ($_POST['op']) { + case t('Save input formats'): + filter_admin_save(); + break; + case t('Add input format'): + filter_admin_add(); + break; + } + + // Overview of all formats + $formats = filter_formats(); + $roles = user_roles(); + $error = false; + + $header = array(t('name'), t('default')); + foreach ($roles as $name) { + $header[] = $name; + } + $header[] = array('data' => t('operations'), 'colspan' => 2); + + $rows = array(); + foreach ($formats as $id => $format) { + $row = array(); + $default = ($id == variable_get('filter_default_format', 1)); + + $row[] = form_textfield('', "name][$id", $format->name, 16, 255); + $row[] = form_radio('', 'default', $id, $default); + + foreach ($roles as $rid => $name) { + $checked = strstr($format->roles, ",$rid,"); + + if ($default && !$checked && !$error) { + form_set_error("roles][$id][$rid", t('The default input format must be accessible to every role.')); + $error = true; + } + + $row[] = form_checkbox('', "roles][$id][$rid", 1, $checked); + } + + $row[] = l('configure', 'admin/filters/'. $id); + $row[] = $default ? '' : l('delete', 'admin/filters/delete/'. $id); + + $rows[] = $row; + } + + $group = theme('table', $header, $rows); + $group .= form_submit(t('Save input formats')); + $output = '<h2>'. t('Permissions and settings') . '</h2>' . form($group); + + // Form to add a new format + $group = t("<p>To add a new input format, type its name here. After it has been added, you can configure its options.</p>"); + $form = form_textfield(t('Name'), 'name', '', 40, 255); + $form .= form_submit(t('Add input format')); + $group .= form($form); + $output .= '<h2>'. t('Add new input format') .'</h2>'. $group; + + print theme('page', $output); +} + +/** + * Save input formats on overview page + */ +function filter_admin_save() { $edit = $_POST['edit']; - $op = $_POST['op']; - if ($op == t('Save configuration')) { - foreach ($edit as $module => $filter) { - db_query("UPDATE {filters} SET weight = %d WHERE module = '%s'", $filter['weight'], $module); + + variable_set('filter_default_format', $edit['default']); + + foreach ($edit['name'] as $id => $name) { + $name = trim($name); + + if (strlen($name) == 0) { + drupal_set_message(t('You must enter a name for this input format.')); + drupal_goto('admin/filters'); + } + else { + db_query("UPDATE {filter_formats} SET name='%s' WHERE format = %d", $name, $id); + } + } + + // We store the roles as a string for ease of use. + // We use leading and trailing comma's to allow easy substring matching. + foreach ($edit['roles'] as $id => $format) { + $roles = ','; + foreach ($format as $rid => $value) { + if ($value) { + $roles .= $rid .','; + } } + db_query("UPDATE {filter_formats} SET roles = '%s' WHERE format = %d", $roles, $id); + } + + drupal_set_message(t('The input format settings have been updated.')); + drupal_goto('admin/filters'); +} + +/** + * Add new input format + */ +function filter_admin_add() { + $edit = $_POST['edit']; + + $name = trim($edit['name']); + + if (strlen($name) == 0) { + drupal_set_message(t('You must enter a name for this input format.')); + drupal_goto('admin/filters'); + } + else { + db_query("INSERT INTO {filter_formats} (name) VALUES ('%s')", $name); + } + + drupal_set_message(t("Added input format '%format'", array('%format' => $edit['name']))); + drupal_goto('admin/filters'); +} + +/** + * Menu callback: confirm deletion of a format + */ +function filter_admin_delete() { + $edit = $_POST['edit']; + if ($_POST['op'] == t('Confirm deletion')) { + if ($edit['format'] != variable_get('filter_default_format', 1)) { + db_query("DELETE FROM {filter_formats} WHERE format = %d", $edit['format']); + db_query("DELETE FROM {filters} WHERE format = %d", $edit['format']); + + $default = variable_get('filter_default_format', 1); + db_query("UPDATE {node} SET format = %d WHERE format = %d", $default, $edit['format']); + db_query("UPDATE {comments} SET format = %d WHERE format = %d", $default, $edit['format']); + db_query("UPDATE {boxes} SET format = %d WHERE format = %d", $default, $edit['format']); + + cache_clear_all('filter:'. $edit['format'], true); + + drupal_set_message(t("Deleted input format '%format'", array('%format' => $edit['name']))); + } + drupal_goto('admin/filters'); + } + + $format = arg(3); + $format = db_fetch_object(db_query('SELECT * FROM {filter_formats} WHERE format = %d', $format)); + + $form .= form_hidden('format', $format->format); + $form .= form_hidden('name', $format->name); + $form .= '<p>'. t("Are you sure you want to delete the input format '%format'? If you have any content left in this input format, it will be switched to the default input format.", array('%format' => $format->name)) .'</p>'; + $form .= form_submit(t('Confirm deletion')); + print theme('page', form($form)); +} + +/** + * Ask for confirmation before deleting a format + */ +function filter_admin_confirm() { + $edit = $_POST['edit']['format']; + + + return form($form); +} + +/** + * Menu callback: configure the filters for a format. + */ +function filter_admin_filters() { + $format = arg(2); + + // Handle saving of weights + if ($_POST['op']) { + filter_admin_filters_save($format, $_POST['edit']); + } + + $all = filter_list_all(); + $enabled = filter_list_format($format); + + /* + ** Table with filters + */ + $header = array('enabled', 'name', 'description'); + $rows = array(); + foreach ($all as $id => $filter) { + $row = array(); + $row[] = form_checkbox('', $id, 1, isset($enabled[$id])); + $row[] = $filter->name; + $row[] = module_invoke($filter->module, 'filter', 'description', $filter->delta); + + $rows[] = $row; + } + $form = theme('table', $header, $rows); + if (!$empty) { + $form .= form_submit(t('Save configuration')); + } + + $output .= '<h2>'. t('Filters') .'</h2>'. form($form); + + /* + ** Compose tips (guidelines) + */ + $tips = _filter_tips($format, false); + $extra = l(t('More information about formatting options'), 'filter/tips'); + $tiplist = theme('filter_tips', $tips, $extra); + if (!$tiplist) { + $tiplist = t('<p>No guidelines available.</p>'); + } + $group = t('<p>These are the guidelines that users will see for posting in this input format. They are automatically generated from the filter settings.</p>'); + $group .= $tiplist; + $output .= '<h2>'. t('Formatting guidelines') .'</h2>'. $group; + + print theme('page', $output); +} + +/** + * Save enabled/disabled status for filters in a format. + */ +function filter_admin_filters_save($format, $toggles) { + $current = filter_list_format($format); + + $cache = true; + + db_query("DELETE FROM {filters} WHERE format = %d", $format); + foreach ($toggles as $id => $checked) { + if ($checked) { + list($module, $delta) = explode('/', $id); + // Add new filters to the bottom + $weight = isset($current[$id]->weight) ? $current[$id]->weight : 10; + db_query("INSERT INTO {filters} (format, module, delta, weight) VALUES (%d, '%s', %d, %d)", $format, $module, $delta, $weight); + + // Check if there are any 'no cache' filters + $cache &= !module_invoke($module, 'filter', 'no cache', $delta); + } + } + + // Update the format's 'no cache' flag. + db_query('UPDATE {filter_formats} SET cache = %d WHERE format = %d', (int)$cache, $format); + + cache_clear_all('filter:'. $format, true); + + drupal_set_message(t('The input format has been updated.')); + drupal_goto('admin/filters/'. arg(2) .'/list'); +} + +/** + * Menu callback: display form for ordering filters for a format. + */ +function filter_admin_order() { + $format = arg(2); + if ($_POST['op']) { + filter_admin_order_save($format, $_POST['edit']); } // Get list (with forced refresh) - filter_refresh(); - $filters = filter_list(); + $filters = filter_list_format($format); $header = array(t('name'), t('weight')); $rows = array(); - // Standard HTML filters are always run first, we add a dummy row to indicate this - $rows[] = array(t('HTML filtering'), array('data' => t('locked'))); - - foreach ($filters as $module => $filter) { - $name = module_invoke($module, 'filter', 'name'); - $rows[] = array($name, array('data' => form_weight(NULL, $module .'][weight', $filter['weight']))); + foreach ($filters as $id => $filter) { + $rows[] = array($filter->name, form_weight('', $id, $filter->weight)); } $form = theme('table', $header, $rows); @@ -99,93 +424,184 @@ function filter_admin_order() { } /** - * Menu callback; displays settings defined by filters. + * Save the weights of filters in a format. + */ +function filter_admin_order_save($format, $weights) { + foreach ($weights as $id => $weight) { + list($module, $delta) = explode('/', $id); + db_query("UPDATE filters SET weight = %d WHERE format = %d AND module = '%s' AND delta = %d", $weight, $format, $module, $delta); + } + drupal_set_message(t('The filter weights have been saved.')); + + cache_clear_all('filter:'. $format, true); + + drupal_goto('admin/filters/'. arg(2) .'/order'); +} + +/** + * Menu callback: display settings defined by filters. */ -function filter_admin_settings() { +function filter_admin_configure() { + $format = arg(2); + system_settings_save(); - filter_refresh(); + $list = filter_list_format($format); + $form = ""; + foreach ($list as $filter) { + $form .= module_invoke($filter->module, 'filter', 'settings', $filter->delta, $format); + } - $form = filter_default_settings(); - $form .= implode("\n", module_invoke_all('filter', 'settings')); - $output = system_settings_form($form); + if (trim($form) != '') { + $output = system_settings_form($form); + } + else { + $output = t('No settings are available.'); + } print theme('page', $output); } /** - * Search through all modules for the filters they implement. + * Retrieve a list of input formats. */ -function filter_refresh() { - $modules = module_list(); - $filters = filter_list(); +function filter_formats() { + global $user; + static $formats; + + // Administrators can always use all input formats. + $all = user_access('administer filters'); + + if (!isset($formats)) { + $formats = array(); + + $query = array('SELECT * FROM {filter_formats}'); - // Update list in database - db_query('DELETE FROM {filters}'); - foreach ($modules as $module) { - if (module_hook($module, 'filter')) { - $weight = $filters[$module]['weight']; + // Build query for selecting the format(s) based on the user's roles + if (!$all) { + $where = array(); + foreach ($user->roles as $rid => $role) { + $where[] = "roles LIKE '%%,%d,%%'"; + $query[] = $rid; + } + $query[0] .= ' WHERE '. implode(' OR ', $where); + } + + $result = call_user_func_array('db_query', $query); + while ($format = db_fetch_object($result)) { + $formats[$format->format] = $format; + } + } + return $formats; +} - db_query("INSERT INTO {filters} (module, weight) VALUES ('%s', %d)", $module, $weight); +/** + * Build a list of all filters. + */ +function filter_list_all() { + $filters = array(); + + foreach (module_list() as $module) { + $list = module_invoke($module, 'filter', 'list'); + if (is_array($list)) { + foreach ($list as $delta => $name) { + $filters[$module .'/'. $delta] = (object)array('module' => $module, 'delta' => $delta, 'name' => $name); + } } } - filter_list(1); + 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->name, $b->name); } /** - * Retrieve a list of all filters from the database. + * Check if text in a certain input format is allowed to be cached. */ -function filter_list($force = 0) { - static $filters; +function filter_format_allowcache($format) { + static $cache = array(); - if (!is_array($filters) || $force) { - $filters = array(); - $result = db_query('SELECT * FROM {filters} ORDER BY weight ASC'); - while ($filter = db_fetch_array($result)) { - // Fail-safe in case a module was deleted/changed without disabling it - if (module_hook($filter['module'], 'filter')) { - $filters[$filter['module']] = $filter; + if (!isset($cache[$format])) { + $cache[$format] = db_result(db_query('SELECT cache FROM {filter_formats} WHERE format = %d', $format)); + } + return $cache[$format]; +} + +/** + * Retrieve a list of filters for a certain format. + */ +function filter_list_format($format) { + static $filters = array(); + + if (!is_array($filters[$format])) { + $filters[$format] = array(); + $result = db_query("SELECT * FROM {filters} WHERE format = %d ORDER BY weight ASC", $format); + while ($filter = db_fetch_object($result)) { + $list = module_invoke($filter->module, 'filter', 'list'); + if (is_array($list) && isset($list[$filter->delta])) { + $filter->name = $list[$filter->delta]; + $filters[$format][$filter->module .'/'. $filter->delta] = $filter; } } } - return $filters; + return $filters[$format]; } +/** + * @name Filtering functions + * + * Modules which need to have content filtered can use these functions to + * interact with the filter system. + * + * @{ + */ + /** * Run all the enabled filters on a piece of text. */ -function check_output($text) { +function check_output($text, $format = FILTER_FORMAT_DEFAULT) { if (isset($text)) { + if ($format == FILTER_FORMAT_DEFAULT) { + $format = variable_get('filter_default_format', 1); + } + + // Check for a cached version of this piece of text + $id = 'filter:'. $format .':'. md5($text); + if ($cached = cache_get($id)) { + return $cached->data; + } + + // See if caching is allowed for this format + $cache = filter_format_allowcache($format); // Convert all Windows and Mac newlines to a single newline, // so filters only need to deal with this one $text = str_replace(array("\r\n", "\r"), "\n", $text); // Get complete list of filters ordered properly - $filters = filter_list(); + $filters = filter_list_format($format); // Give filters the chance to escape HTML-like data such as code or formulas. - // From this point on, the input can be treated as HTML. - if (variable_get('filter_html', FILTER_HTML_DONOTHING) != FILTER_HTML_ESCAPE) { - foreach ($filters as $module => $filter) { - $text = module_invoke($module, 'filter', 'prepare', $text); - } + foreach ($filters as $filter) { + $text = module_invoke($filter->module, 'filter', 'prepare', $filter->delta, $format, $text); } - // HTML handling is done before all regular filtering activities. - $text = filter_default($text); - - // Regular filtering. - foreach ($filters as $module => $filter) { - $text = module_invoke($module, 'filter', 'process', $text); + // Perform filtering + foreach ($filters as $filter) { + $text = module_invoke($filter->module, 'filter', 'process', $filter->delta, $format, $text); } - // If only inline elements are used and no block level elements, we - // replace all newlines with HTML line breaks. - if (strip_tags($text, '<a><br><span><bdo><map><object><img><tt><i><b><u><big><small><em><strong><dfn><code><q><samp><kbd><var><cite><abbr><acronym><sub><sup><input><select><textarea><label><button><ins><del><script>') == $text) { - $text = nl2br($text); + // Store in cache + if ($cache) { + cache_set($id, $text, 1); } } else { @@ -196,59 +612,255 @@ function check_output($text) { } /** - * Perform the default filters, preventing malicious HTML from being displayed. + * Generate selector for choosing a format in a form. + * + * @param $name + * The internal name used to refer to the selector. + * + * @param $value + * The id of the format that is currently selected. + * + * @return + * HTML for the selector. */ -function filter_default($text) { - if (variable_get('filter_html', FILTER_HTML_DONOTHING) == FILTER_HTML_STRIP) { - // Allow users to enter HTML, but filter it - $text = strip_tags($text, variable_get('allowed_html', '')); - if (variable_get('filter_style', FILTER_STYLE_STRIP)) { - $text = preg_replace('/\Wstyle\s*=[^>]+?>/i', '>', $text); +function filter_form($name, $value = FILTER_FORMAT_DEFAULT) { + if ($value == FILTER_FORMAT_DEFAULT) { + $value = variable_get('filter_default_format', 1); + } + $formats = filter_formats(); + + $extra = l(t('More information about formatting options'), 'filter/tips'); + + if (count($formats) > 1) { + // Multiple formats available: display radio buttons with tips. + $output = ''; + foreach ($formats as $format) { + $tips = _filter_tips($format->format, false); + + // TODO: get support for block-level radios so the <br /> is not output? + $output .= '<div>'; + $output .= '<label class="option"><input type="radio" class="form-radio" name="edit['. $name .']" value="'. $format->format .'"'. ($format->format == $value ? ' checked="checked"' : '') .' /> '. $format->name .'</label>'; + $output .= theme('filter_tips', $tips); + $output .= '</div>'; } - $text = preg_replace('/\Won[a-z]+\s*=[^>]+?>/i', '>', $text); + return theme('form_element', t('Input format'), $output, $extra, $name, _form_get_error($name)); } + else { + // Only one format available: use a hidden form item and only show tips. + $format = array_shift($formats); + $output = form_hidden($name, $format->format); + $tips = _filter_tips(variable_get('filter_default_format', 0), false); + $output .= form_item(t('Formatting guidelines'), theme('filter_tips', $tips, $extra), $extra); + return $output; + } +} - if (variable_get('filter_html', FILTER_HTML_DONOTHING) == FILTER_HTML_ESCAPE) { - // Escape HTML - $text = htmlspecialchars($text); +/** + * Returns true if the user is allowed to access this format. + */ +function filter_access($format) { + if (user_access('administer filters')) { + return true; } + else { + $formats = filter_formats(); + return isset($formats[$format]); + } +} +/* @} */ - return trim($text); +/** + * Menu callback: show page with long filter tips + */ +function filter_tips_long() { + $format = arg(2); + if ($format) { + $output = theme('filter_tips', _filter_tips($format, true)); + } + else { + $output = theme('filter_tips', _filter_tips(-1, true)); + } + print theme('page', $output, t('Compose Tips')); +} + +/** + * Helper function for fetching filter tips. + */ +function _filter_tips($format, $long = false) { + if ($format == -1) { + $formats = filter_formats(); + } + else { + $formats = array(db_fetch_object(db_query("SELECT * FROM {filter_formats} WHERE format = %d", $format))); + } + + $tips = array(); + + foreach ($formats as $format) { + $filters = filter_list_format($format->format); + + $tips[$format->name] = array(); + foreach ($filters as $id => $filter) { + if ($tip = module_invoke($filter->module, 'filter_tips', $filter->delta, $format->format, $long)) { + $tips[$format->name][] = array('tip' => $tip, 'id' => $id); + } + } + } + + return $tips; } /** - * Settings for the filter system's built-in HTML handling. + * @addtogroup themeable + * @{ */ -function filter_default_settings() { - $group = form_radios(t('Filter HTML tags'), 'filter_html', variable_get('filter_html', FILTER_HTML_DONOTHING), array(FILTER_HTML_DONOTHING => t('Do not filter'), FILTER_HTML_STRIP => t('Strip tags'), FILTER_HTML_ESCAPE => t('Escape tags')), t('How to deal with HTML and PHP tags in user-contributed content. If set to "Strip tags", dangerous tags are removed (see below). If set to "Escape tags", all HTML is escaped and presented as it was typed.')); - $group .= form_textfield(t('Allowed HTML tags'), 'allowed_html', variable_get('allowed_html', '<a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul>'), 64, 255, t('If "Strip tags" is selected, optionally specify tags which should not be stripped. "ON*" attributes are always stripped.')); - $group .= form_radios(t('HTML style attributes'), 'filter_style', variable_get('filter_style', FILTER_STYLE_STRIP), array(FILTER_STYLE_ALLOW => t('Allowed'), FILTER_STYLE_STRIP => t('Removed')), t('If "Strip tags" is selected, you can choose whether "STYLE" attributes are allowed or removed from input.')); - $output .= form_group(t('HTML filtering'), $group); + +/** + * Format filter tips + * + * @param $tips + * @param $long + * @param $extra + */ +function theme_filter_tips($tips, $long = false, $extra = '') { + $output = ''; + + $multiple = count($tips) > 1; + if ($multiple) { + $output = t('Input formats') .':'; + } + + if (count($tips)) { + if ($multiple) { + $output .= '<ul>'; + } + foreach ($tips as $name => $tiplist) { + if ($multiple) { + $output .= '<li>'; + $output .= '<strong>'. $name .'</strong>:<br />'; + } + + $output .= '<ul class="tips">'; + + foreach ($tiplist as $tip) { + $output .= '<li'. ($long ? ' id="'. $tip['id'] .'">' : '>') . $tip['tip'] . '</li>'; + } + + $output .= '</ul>'; + if ($multiple) { + $output .= '</li>'; + } + } + if ($multiple) { + $output .= '</ul>'; + } + } return $output; } +/** @} End of addtogroup themeable */ + /** - * Implementation of hook_filter(). Handles URL upgrades from Drupal 4.1. + * @name Standard filters + * + * Filters implemented by the filter.module. + * @{ */ -function filter_filter($op, $text = '') { + +/** + * Implementation of hook_filter(). Contains a basic set of essential filters: + * - HTML filter: transform/validate user-supplied HTML + * - PHP evaluator: execute PHP code + * - Legacy filter: handle URL upgrades from Drupal 4.1. + * - Line break convertor: convert newlines into paragraph and break tags + */ +function filter_filter($op, $delta = 0, $format = -1, $text = '') { switch ($op) { - case 'name': - return t('Legacy filtering'); + case 'list': + return array(0 => t('HTML filter'), 1 => t('PHP evaluator'), 2 => t('Legacy filter'), 3 => t('Line break convertor')); + + case 'no cache': + return $delta == 1; // No caching for the PHP evaluator + + case 'description': + switch ($delta) { + case 0: + return t('Allows you to restrict if users can post HTML and which tags to filter out.'); + case 1: + return t('Runs a piece of PHP code. The usage of this filter should be restricted to administrators only!'); + case 2: + return t('Replaces URLs from Drupal 4.1 (and lower) with updated equivalents.'); + case 3: + return t('Converts line breaks into HTML (i.e. <br> and <p> tags).'); + default: + return; + } + case 'process': - if (variable_get('rewrite_old_urls', 0)) { - $text = filter_old_urls($text); + switch ($delta) { + case 0: + return _filter_html($text, $format); + case 1: + return drupal_eval($text); + case 2: + return _filter_old_urls($text, $format); + case 3: + return _filter_autop($text, $format); + default: + return $text; } - return $text; + case 'settings': - $group = form_radios(t('Rewrite old URLs'), 'rewrite_old_urls', variable_get('rewrite_old_urls', 0), array(t('Disabled'), t('Enabled')), t('The introduction of "clean URLs" in Drupal 4.2.0 breaks internal URLs that date back from Drupal 4.1.0 and before. If enabled, this filter will attempt to rewrite the old style URLs to avoid broken links. If <code>mod_rewrite</code> is available on your system, use the rewrite rules in Drupal\'s <code>.htaccess</code> file instead as these will also correct external referrers.')); - $output .= form_group(t('Legacy filtering'), $group); - return $output; + switch ($delta) { + case 0: + return _filter_html_settings($format); + default: + return; + } + default: return $text; } } +/** + * Settings for the HTML filter + */ +function _filter_html_settings($format) { + $group = form_radios(t('Filter HTML tags'), "filter_html_$format", variable_get("filter_html_$format", FILTER_HTML_STRIP), array(FILTER_HTML_STRIP => t('Strip tags'), FILTER_HTML_ESCAPE => t('Escape tags')), t('How to deal with HTML tags in user-contributed content. If set to "Strip tags", dangerous tags are removed (see below). If set to "Escape tags", all HTML is escaped and presented as it was typed.')); + $group .= form_textfield(t('Allowed HTML tags'), "allowed_html_$format", variable_get("allowed_html_$format", '<a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul>'), 64, 255, t('If "Strip tags" is selected, optionally specify tags which should not be stripped. Javascript event attributes are always stripped.')); + $group .= form_radios(t('HTML style attributes'), "filter_style_$format", variable_get("filter_style_$format", FILTER_STYLE_STRIP), array(FILTER_STYLE_ALLOW => t('Allowed'), FILTER_STYLE_STRIP => t('Removed')), t('If "Strip tags" is selected, you can choose whether "STYLE" attributes are allowed or removed from input.')); + $output .= form_group(t('HTML filter'), $group); + + return $output; +} + +/** + * HTML filter: provides filtering of input into accepted HTML + */ +function _filter_html($text, $format) { + if (variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_STRIP) { + // Allow users to enter HTML, but filter it + $text = strip_tags($text, variable_get("allowed_html_$format", '')); + if (variable_get("filter_style_$format", FILTER_STYLE_STRIP)) { + $text = preg_replace('/\Wstyle\s*=[^>]+?>/i', '>', $text); + } + $text = preg_replace('/\Won[a-z]+\s*=[^>]+?>/i', '>', $text); + } + + if (variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_ESCAPE) { + // Escape HTML + $text = htmlspecialchars($text); + } + + if (variable_get("filter_nl2br_$format", true)) { + $text = _filter_autop($text, true); + } + + return trim($text); +} + /** * Rewrite legacy URLs. * @@ -258,7 +870,11 @@ function filter_filter($op, $text = '') { * enough, we will use them to permanently rewrite the links in node * and comment bodies. */ -function filter_old_urls($text) { +function _filter_old_urls($text) { + if (!variable_get('rewrite_old_urls', 0)) { + return $text; + } + global $base_url; $end = substr($base_url, 12); @@ -310,31 +926,30 @@ function filter_old_urls($text) { } /** - * Fetch full filter help texts defined by modules. + * Convert line breaks into <p> and <br> in an intelligent fashion. + * From: http://photomatt.net/scripts/autop */ -function filter_tips_long() { - $tiplist = ''; - foreach (module_list() as $name) { - if ($tip = module_invoke($name, 'help', 'filter#long-tip')) { - $tiplist .= "<li id=\"filter-$name\">$tip</li>\n"; - } - } - $output = "<ul class=\"filter-tips-long\">\n$tiplist\n</ul>\n"; - print theme('page', $output, t('Compose Tips')); -} +function _filter_autop($text, $br = 1) { + $text = preg_replace('|\n*$|', '', $text) ."\n\n"; // just to make things a little easier, pad the end + $text = preg_replace('|<br />\s*<br />|', "\n\n", $text); + $text = preg_replace('!(<(?:table|ul|ol|li|pre|form|blockquote|h[1-6])[^>]*>)!', "\n$1", $text); // Space things out a little + $text = preg_replace('!(</(?:table|ul|ol|li|pre|form|blockquote|h[1-6])>)!', "$1\n", $text); // Space things out a little + $text = preg_replace("/\n\n+/", "\n\n", $text); // take care of duplicates + $text = preg_replace('/\n?(.+?)(?:\n\s*\n|\z)/s', "\t<p>$1</p>\n", $text); // make paragraphs, including one at the end + $text = preg_replace('|<p>\s*?</p>|', '', $text); // under certain strange conditions it could create a P of entirely whitespace + $text = preg_replace("|<p>(<li.+?)</p>|", "$1", $text); // problem with nested lists + $text = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $text); + $text = str_replace('</blockquote></p>', '</p></blockquote>', $text); + $text = preg_replace('!<p>\s*(</?(?:table|tr|td|th|div|ul|ol|li|pre|select|form|blockquote|p|h[1-6])[^>]*>)!', "$1", $text); + $text = preg_replace('!(</?(?:table|tr|td|th|div|ul|ol|li|pre|select|form|blockquote|p|h[1-6])[^>]*>)\s*</p>!', "$1", $text); + if ($br) $text = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $text); // optionally make line breaks + $text = preg_replace('!(</?(?:table|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|form|blockquote|p|h[1-6])[^>]*>)\s*<br />!', "$1", $text); + $text = preg_replace('!<br />(\s*</?(?:p|li|div|th|pre|td|ul|ol)>)!', '$1', $text); + $text = preg_replace('/&([^#])(?![a-z]{1,8};)/', '&$1', $text); -/** - * Fetch abbreviated filter help texts defined by modules. - */ -function filter_tips_short() { - $tiplist = ''; - foreach (module_list() as $name) { - if ($tip = module_invoke($name, 'help', 'filter#short-tip')) { - $tiplist .= "<li>$tip</li>\n"; - } - } - $tiplist .= '<li class="more-tips">' . l(t('More information on formatting options'), 'filter/tips') . '</li>'; - return "<ul class=\"filter-tips-short\">\n$tiplist\n</ul>\n"; + return $text; } +/* @} */ + ?> diff --git a/modules/filter/filter.module b/modules/filter/filter.module index dbb2eab99f3f6291c6c4a1c39043d53967cfdeff..f5e0834c1c6e6371297b0a6d0201580c7471eb80 100644 --- a/modules/filter/filter.module +++ b/modules/filter/filter.module @@ -1,7 +1,8 @@ <?php // $Id$ -define('FILTER_HTML_DONOTHING', 0); +define('FILTER_FORMAT_DEFAULT', 0); + define('FILTER_HTML_STRIP', 1); define('FILTER_HTML_ESCAPE', 2); @@ -12,52 +13,135 @@ * Implementation of hook_help(). */ function filter_help($section) { + // Get rid of variable numbers in the URL + $section = preg_replace('/[0-9]+/', '#', $section); + switch ($section) { case 'admin/modules#description': return t('Framework for handling filtering of content.'); + case 'admin/filters': - return t(" -<p>Filters fit between the raw text in posts and comments, and the HTML output. They allow you to replace text selectively. Uses include automatic conversion of emoticons into graphics and filtering HTML content from users' submissions.</p> -<p>If you notice some filters are causing conflicts in the output, you can <a href=\"%url\">rearrange them</a>.</p>", array('%url' => url('admin/filters/order'))); - case 'admin/filters/order': - return t(" + return t(' +<p><i>Input formats</i> define a way of processing user-supplied text in Drupal. Every input format has its own settings of which <i>filters</i> to apply. Possible filters include stripping out malicious HTML and making URLs clickable.</p> +<p>Users can choose between the available input formats when submitting content.</p> +<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>'); + + case 'admin/filters/#': + return t(' +<p>Every <i>filter</i> 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> +<p>If you notice some filters are causing conflicts in the output, you can <a href="%order">rearrange them</a>.', array('%configure' => url('admin/filters/'. arg(2) .'/configure'), '%order' => url('admin/filters/'. arg(2) .'/order'))); + + case 'admin/filters/#/configure': + return t(' +<p>If you cannot find the settings for a certain filter, make sure you\'ve enabled it on the <a href="%url">list filters</a> tab first.</p>', array('%url' => url('admin/filters/'. arg(2) .'/list'))); + + case 'admin/filters/#/order': + return t(' <p>Because of the flexible filtering system, you might encounter a situation where one filter prevents another from doing its job. For example: a word in an URL gets converted into a glossary term, before the URL can be converted in a clickable link. When this happens, you will need to rearrange the order in which filters get executed.</p> -<p>Filters are executed from top-to-bottom. You can use the weight column to rearrange them: heavier filters 'sink' to the bottom. Standard HTML filtering is always run first.</p>"); - case 'filter#long-tip': - case 'filter#short-tip': - switch (variable_get('filter_html', FILTER_HTML_DONOTHING)) { - case 0: - return t('All HTML tags allowed'); - break; - case 1: - if ($allowed_html = variable_get('allowed_html', '<a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul>')) { +<p>Filters are executed from top-to-bottom. You can use the weight column to rearrange them: heavier filters \'sink\' to the bottom.</p>'); + } +} + +/** + * Implementation of hook_filter_tips. + */ +function filter_filter_tips($delta, $format, $type = false) { + switch ($delta) { + case 0: + switch (variable_get("filter_html_$format", FILTER_HTML_STRIP)) { + + case FILTER_HTML_STRIP: + if ($allowed_html = variable_get("allowed_html_$format", '<a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul>')) { return t('Allowed HTML tags') .': '. htmlspecialchars($allowed_html); - } else { + } + else { return t('No HTML tags allowed'); } - break; - case 2: + + case FILTER_STYLE_STRIP: return t('No HTML tags allowed'); - break; } break; + + case 1: + switch ($type) { + case 0: + return t('You may post PHP code. You should include <?php ?> tags.'); + case 1: + return t(' +<h4>Using custom PHP code</h4> +<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 don\'t 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 don\'t plan to do fancy stuff with your content then you\'re probably better off with straight HTML.</p> +<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> +<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> +<p>A basic example:</p> +<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> +<pre> + print t("Welcome visitor, ... welcome message goes here ..."); +</pre> +<p>If we are however dealing with a registered user, we can customize the message by using:</p> +<pre> + global $user; + if ($user->uid) { + print t("Welcome $user->name, ... welcome message goes here ..."); + } + else { + print t("Welcome visitor, ... welcome message goes here ..."); + } +</pre></blockquote> +<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>'); + } + + case 3: + return t('Lines and paragraphs break automatically.'); + break; } } + /** * Implementation of hook_menu(). */ function filter_menu() { $items = array(); - $items[] = array('path' => 'admin/filters', 'title' => t('filters'), - 'callback' => 'filter_admin_settings', - 'access' => user_access('administer site configuration')); - $items[] = array('path' => 'admin/filters/configure', 'title' => t('configure'), - 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); - $items[] = array('path' => 'admin/filters/order', 'title' => t('rearrange'), - 'callback' => 'filter_admin_order', - 'access' => user_access('administer site configuration'), - 'type' => MENU_LOCAL_TASK); + + $items[] = array('path' => 'admin/filters', 'title' => t('input formats'), + 'callback' => 'filter_admin_overview', + 'access' => user_access('administer filters')); + + $items[] = array('path' => 'admin/filters/delete', 'title' => t('delete input format'), + 'callback' => 'filter_admin_delete', + 'type' => MENU_CALLBACK, + 'access' => user_access('administer filters')); + + if (arg(0) == 'admin' && arg(1) == 'filters' && is_numeric(arg(2))) { + $formats = filter_formats(); + + if (isset($formats[arg(2)])) { + $items[] = array('path' => 'admin/filters/'. arg(2), 'title' => t("'%format' input format", array('%format' => $formats[arg(2)]->name)), + 'callback' => 'filter_admin_filters', + 'type' => MENU_CALLBACK, + 'access' => user_access('administer filters')); + + $items[] = array('path' => 'admin/filters/'. arg(2) .'/list', 'title' => t('list filters'), + 'callback' => 'filter_admin_filters', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => 0, + 'access' => user_access('administer filters')); + + $items[] = array('path' => 'admin/filters/'. arg(2) .'/configure', 'title' => t('configure filters'), + 'callback' => 'filter_admin_configure', + 'type' => MENU_LOCAL_TASK, + 'weight' => 1, + 'access' => user_access('administer filters')); + + $items[] = array('path' => 'admin/filters/'. arg(2) .'/order', 'title' => t('rearrange filters'), + 'callback' => 'filter_admin_order', + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, + 'access' => user_access('administer filters')); + } + } + $items[] = array('path' => 'filter/tips', 'title' => t('compose tips'), 'callback' => 'filter_tips_long', 'access' => TRUE, 'type' => MENU_SUGGESTED_ITEM); @@ -65,30 +149,271 @@ function filter_menu() { } /** - * Menu callback; allows administrators to change the filter ordering. + * Implementation of hook_perm() */ -function filter_admin_order() { +function filter_perm() { + return array('administer filters'); +} + +/** + * Menu callback: allows administrators to setup input formats + */ +function filter_admin_overview() { + // Process form submission + switch ($_POST['op']) { + case t('Save input formats'): + filter_admin_save(); + break; + case t('Add input format'): + filter_admin_add(); + break; + } + + // Overview of all formats + $formats = filter_formats(); + $roles = user_roles(); + $error = false; + + $header = array(t('name'), t('default')); + foreach ($roles as $name) { + $header[] = $name; + } + $header[] = array('data' => t('operations'), 'colspan' => 2); + + $rows = array(); + foreach ($formats as $id => $format) { + $row = array(); + $default = ($id == variable_get('filter_default_format', 1)); + + $row[] = form_textfield('', "name][$id", $format->name, 16, 255); + $row[] = form_radio('', 'default', $id, $default); + + foreach ($roles as $rid => $name) { + $checked = strstr($format->roles, ",$rid,"); + + if ($default && !$checked && !$error) { + form_set_error("roles][$id][$rid", t('The default input format must be accessible to every role.')); + $error = true; + } + + $row[] = form_checkbox('', "roles][$id][$rid", 1, $checked); + } + + $row[] = l('configure', 'admin/filters/'. $id); + $row[] = $default ? '' : l('delete', 'admin/filters/delete/'. $id); + + $rows[] = $row; + } + + $group = theme('table', $header, $rows); + $group .= form_submit(t('Save input formats')); + $output = '<h2>'. t('Permissions and settings') . '</h2>' . form($group); + + // Form to add a new format + $group = t("<p>To add a new input format, type its name here. After it has been added, you can configure its options.</p>"); + $form = form_textfield(t('Name'), 'name', '', 40, 255); + $form .= form_submit(t('Add input format')); + $group .= form($form); + $output .= '<h2>'. t('Add new input format') .'</h2>'. $group; + + print theme('page', $output); +} + +/** + * Save input formats on overview page + */ +function filter_admin_save() { $edit = $_POST['edit']; - $op = $_POST['op']; - if ($op == t('Save configuration')) { - foreach ($edit as $module => $filter) { - db_query("UPDATE {filters} SET weight = %d WHERE module = '%s'", $filter['weight'], $module); + + variable_set('filter_default_format', $edit['default']); + + foreach ($edit['name'] as $id => $name) { + $name = trim($name); + + if (strlen($name) == 0) { + drupal_set_message(t('You must enter a name for this input format.')); + drupal_goto('admin/filters'); + } + else { + db_query("UPDATE {filter_formats} SET name='%s' WHERE format = %d", $name, $id); + } + } + + // We store the roles as a string for ease of use. + // We use leading and trailing comma's to allow easy substring matching. + foreach ($edit['roles'] as $id => $format) { + $roles = ','; + foreach ($format as $rid => $value) { + if ($value) { + $roles .= $rid .','; + } } + db_query("UPDATE {filter_formats} SET roles = '%s' WHERE format = %d", $roles, $id); + } + + drupal_set_message(t('The input format settings have been updated.')); + drupal_goto('admin/filters'); +} + +/** + * Add new input format + */ +function filter_admin_add() { + $edit = $_POST['edit']; + + $name = trim($edit['name']); + + if (strlen($name) == 0) { + drupal_set_message(t('You must enter a name for this input format.')); + drupal_goto('admin/filters'); + } + else { + db_query("INSERT INTO {filter_formats} (name) VALUES ('%s')", $name); + } + + drupal_set_message(t("Added input format '%format'", array('%format' => $edit['name']))); + drupal_goto('admin/filters'); +} + +/** + * Menu callback: confirm deletion of a format + */ +function filter_admin_delete() { + $edit = $_POST['edit']; + if ($_POST['op'] == t('Confirm deletion')) { + if ($edit['format'] != variable_get('filter_default_format', 1)) { + db_query("DELETE FROM {filter_formats} WHERE format = %d", $edit['format']); + db_query("DELETE FROM {filters} WHERE format = %d", $edit['format']); + + $default = variable_get('filter_default_format', 1); + db_query("UPDATE {node} SET format = %d WHERE format = %d", $default, $edit['format']); + db_query("UPDATE {comments} SET format = %d WHERE format = %d", $default, $edit['format']); + db_query("UPDATE {boxes} SET format = %d WHERE format = %d", $default, $edit['format']); + + cache_clear_all('filter:'. $edit['format'], true); + + drupal_set_message(t("Deleted input format '%format'", array('%format' => $edit['name']))); + } + drupal_goto('admin/filters'); + } + + $format = arg(3); + $format = db_fetch_object(db_query('SELECT * FROM {filter_formats} WHERE format = %d', $format)); + + $form .= form_hidden('format', $format->format); + $form .= form_hidden('name', $format->name); + $form .= '<p>'. t("Are you sure you want to delete the input format '%format'? If you have any content left in this input format, it will be switched to the default input format.", array('%format' => $format->name)) .'</p>'; + $form .= form_submit(t('Confirm deletion')); + print theme('page', form($form)); +} + +/** + * Ask for confirmation before deleting a format + */ +function filter_admin_confirm() { + $edit = $_POST['edit']['format']; + + + return form($form); +} + +/** + * Menu callback: configure the filters for a format. + */ +function filter_admin_filters() { + $format = arg(2); + + // Handle saving of weights + if ($_POST['op']) { + filter_admin_filters_save($format, $_POST['edit']); + } + + $all = filter_list_all(); + $enabled = filter_list_format($format); + + /* + ** Table with filters + */ + $header = array('enabled', 'name', 'description'); + $rows = array(); + foreach ($all as $id => $filter) { + $row = array(); + $row[] = form_checkbox('', $id, 1, isset($enabled[$id])); + $row[] = $filter->name; + $row[] = module_invoke($filter->module, 'filter', 'description', $filter->delta); + + $rows[] = $row; + } + $form = theme('table', $header, $rows); + if (!$empty) { + $form .= form_submit(t('Save configuration')); + } + + $output .= '<h2>'. t('Filters') .'</h2>'. form($form); + + /* + ** Compose tips (guidelines) + */ + $tips = _filter_tips($format, false); + $extra = l(t('More information about formatting options'), 'filter/tips'); + $tiplist = theme('filter_tips', $tips, $extra); + if (!$tiplist) { + $tiplist = t('<p>No guidelines available.</p>'); + } + $group = t('<p>These are the guidelines that users will see for posting in this input format. They are automatically generated from the filter settings.</p>'); + $group .= $tiplist; + $output .= '<h2>'. t('Formatting guidelines') .'</h2>'. $group; + + print theme('page', $output); +} + +/** + * Save enabled/disabled status for filters in a format. + */ +function filter_admin_filters_save($format, $toggles) { + $current = filter_list_format($format); + + $cache = true; + + db_query("DELETE FROM {filters} WHERE format = %d", $format); + foreach ($toggles as $id => $checked) { + if ($checked) { + list($module, $delta) = explode('/', $id); + // Add new filters to the bottom + $weight = isset($current[$id]->weight) ? $current[$id]->weight : 10; + db_query("INSERT INTO {filters} (format, module, delta, weight) VALUES (%d, '%s', %d, %d)", $format, $module, $delta, $weight); + + // Check if there are any 'no cache' filters + $cache &= !module_invoke($module, 'filter', 'no cache', $delta); + } + } + + // Update the format's 'no cache' flag. + db_query('UPDATE {filter_formats} SET cache = %d WHERE format = %d', (int)$cache, $format); + + cache_clear_all('filter:'. $format, true); + + drupal_set_message(t('The input format has been updated.')); + drupal_goto('admin/filters/'. arg(2) .'/list'); +} + +/** + * Menu callback: display form for ordering filters for a format. + */ +function filter_admin_order() { + $format = arg(2); + if ($_POST['op']) { + filter_admin_order_save($format, $_POST['edit']); } // Get list (with forced refresh) - filter_refresh(); - $filters = filter_list(); + $filters = filter_list_format($format); $header = array(t('name'), t('weight')); $rows = array(); - // Standard HTML filters are always run first, we add a dummy row to indicate this - $rows[] = array(t('HTML filtering'), array('data' => t('locked'))); - - foreach ($filters as $module => $filter) { - $name = module_invoke($module, 'filter', 'name'); - $rows[] = array($name, array('data' => form_weight(NULL, $module .'][weight', $filter['weight']))); + foreach ($filters as $id => $filter) { + $rows[] = array($filter->name, form_weight('', $id, $filter->weight)); } $form = theme('table', $header, $rows); @@ -99,93 +424,184 @@ function filter_admin_order() { } /** - * Menu callback; displays settings defined by filters. + * Save the weights of filters in a format. + */ +function filter_admin_order_save($format, $weights) { + foreach ($weights as $id => $weight) { + list($module, $delta) = explode('/', $id); + db_query("UPDATE filters SET weight = %d WHERE format = %d AND module = '%s' AND delta = %d", $weight, $format, $module, $delta); + } + drupal_set_message(t('The filter weights have been saved.')); + + cache_clear_all('filter:'. $format, true); + + drupal_goto('admin/filters/'. arg(2) .'/order'); +} + +/** + * Menu callback: display settings defined by filters. */ -function filter_admin_settings() { +function filter_admin_configure() { + $format = arg(2); + system_settings_save(); - filter_refresh(); + $list = filter_list_format($format); + $form = ""; + foreach ($list as $filter) { + $form .= module_invoke($filter->module, 'filter', 'settings', $filter->delta, $format); + } - $form = filter_default_settings(); - $form .= implode("\n", module_invoke_all('filter', 'settings')); - $output = system_settings_form($form); + if (trim($form) != '') { + $output = system_settings_form($form); + } + else { + $output = t('No settings are available.'); + } print theme('page', $output); } /** - * Search through all modules for the filters they implement. + * Retrieve a list of input formats. */ -function filter_refresh() { - $modules = module_list(); - $filters = filter_list(); +function filter_formats() { + global $user; + static $formats; + + // Administrators can always use all input formats. + $all = user_access('administer filters'); + + if (!isset($formats)) { + $formats = array(); + + $query = array('SELECT * FROM {filter_formats}'); - // Update list in database - db_query('DELETE FROM {filters}'); - foreach ($modules as $module) { - if (module_hook($module, 'filter')) { - $weight = $filters[$module]['weight']; + // Build query for selecting the format(s) based on the user's roles + if (!$all) { + $where = array(); + foreach ($user->roles as $rid => $role) { + $where[] = "roles LIKE '%%,%d,%%'"; + $query[] = $rid; + } + $query[0] .= ' WHERE '. implode(' OR ', $where); + } + + $result = call_user_func_array('db_query', $query); + while ($format = db_fetch_object($result)) { + $formats[$format->format] = $format; + } + } + return $formats; +} - db_query("INSERT INTO {filters} (module, weight) VALUES ('%s', %d)", $module, $weight); +/** + * Build a list of all filters. + */ +function filter_list_all() { + $filters = array(); + + foreach (module_list() as $module) { + $list = module_invoke($module, 'filter', 'list'); + if (is_array($list)) { + foreach ($list as $delta => $name) { + $filters[$module .'/'. $delta] = (object)array('module' => $module, 'delta' => $delta, 'name' => $name); + } } } - filter_list(1); + 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->name, $b->name); } /** - * Retrieve a list of all filters from the database. + * Check if text in a certain input format is allowed to be cached. */ -function filter_list($force = 0) { - static $filters; +function filter_format_allowcache($format) { + static $cache = array(); - if (!is_array($filters) || $force) { - $filters = array(); - $result = db_query('SELECT * FROM {filters} ORDER BY weight ASC'); - while ($filter = db_fetch_array($result)) { - // Fail-safe in case a module was deleted/changed without disabling it - if (module_hook($filter['module'], 'filter')) { - $filters[$filter['module']] = $filter; + if (!isset($cache[$format])) { + $cache[$format] = db_result(db_query('SELECT cache FROM {filter_formats} WHERE format = %d', $format)); + } + return $cache[$format]; +} + +/** + * Retrieve a list of filters for a certain format. + */ +function filter_list_format($format) { + static $filters = array(); + + if (!is_array($filters[$format])) { + $filters[$format] = array(); + $result = db_query("SELECT * FROM {filters} WHERE format = %d ORDER BY weight ASC", $format); + while ($filter = db_fetch_object($result)) { + $list = module_invoke($filter->module, 'filter', 'list'); + if (is_array($list) && isset($list[$filter->delta])) { + $filter->name = $list[$filter->delta]; + $filters[$format][$filter->module .'/'. $filter->delta] = $filter; } } } - return $filters; + return $filters[$format]; } +/** + * @name Filtering functions + * + * Modules which need to have content filtered can use these functions to + * interact with the filter system. + * + * @{ + */ + /** * Run all the enabled filters on a piece of text. */ -function check_output($text) { +function check_output($text, $format = FILTER_FORMAT_DEFAULT) { if (isset($text)) { + if ($format == FILTER_FORMAT_DEFAULT) { + $format = variable_get('filter_default_format', 1); + } + + // Check for a cached version of this piece of text + $id = 'filter:'. $format .':'. md5($text); + if ($cached = cache_get($id)) { + return $cached->data; + } + + // See if caching is allowed for this format + $cache = filter_format_allowcache($format); // Convert all Windows and Mac newlines to a single newline, // so filters only need to deal with this one $text = str_replace(array("\r\n", "\r"), "\n", $text); // Get complete list of filters ordered properly - $filters = filter_list(); + $filters = filter_list_format($format); // Give filters the chance to escape HTML-like data such as code or formulas. - // From this point on, the input can be treated as HTML. - if (variable_get('filter_html', FILTER_HTML_DONOTHING) != FILTER_HTML_ESCAPE) { - foreach ($filters as $module => $filter) { - $text = module_invoke($module, 'filter', 'prepare', $text); - } + foreach ($filters as $filter) { + $text = module_invoke($filter->module, 'filter', 'prepare', $filter->delta, $format, $text); } - // HTML handling is done before all regular filtering activities. - $text = filter_default($text); - - // Regular filtering. - foreach ($filters as $module => $filter) { - $text = module_invoke($module, 'filter', 'process', $text); + // Perform filtering + foreach ($filters as $filter) { + $text = module_invoke($filter->module, 'filter', 'process', $filter->delta, $format, $text); } - // If only inline elements are used and no block level elements, we - // replace all newlines with HTML line breaks. - if (strip_tags($text, '<a><br><span><bdo><map><object><img><tt><i><b><u><big><small><em><strong><dfn><code><q><samp><kbd><var><cite><abbr><acronym><sub><sup><input><select><textarea><label><button><ins><del><script>') == $text) { - $text = nl2br($text); + // Store in cache + if ($cache) { + cache_set($id, $text, 1); } } else { @@ -196,59 +612,255 @@ function check_output($text) { } /** - * Perform the default filters, preventing malicious HTML from being displayed. + * Generate selector for choosing a format in a form. + * + * @param $name + * The internal name used to refer to the selector. + * + * @param $value + * The id of the format that is currently selected. + * + * @return + * HTML for the selector. */ -function filter_default($text) { - if (variable_get('filter_html', FILTER_HTML_DONOTHING) == FILTER_HTML_STRIP) { - // Allow users to enter HTML, but filter it - $text = strip_tags($text, variable_get('allowed_html', '')); - if (variable_get('filter_style', FILTER_STYLE_STRIP)) { - $text = preg_replace('/\Wstyle\s*=[^>]+?>/i', '>', $text); +function filter_form($name, $value = FILTER_FORMAT_DEFAULT) { + if ($value == FILTER_FORMAT_DEFAULT) { + $value = variable_get('filter_default_format', 1); + } + $formats = filter_formats(); + + $extra = l(t('More information about formatting options'), 'filter/tips'); + + if (count($formats) > 1) { + // Multiple formats available: display radio buttons with tips. + $output = ''; + foreach ($formats as $format) { + $tips = _filter_tips($format->format, false); + + // TODO: get support for block-level radios so the <br /> is not output? + $output .= '<div>'; + $output .= '<label class="option"><input type="radio" class="form-radio" name="edit['. $name .']" value="'. $format->format .'"'. ($format->format == $value ? ' checked="checked"' : '') .' /> '. $format->name .'</label>'; + $output .= theme('filter_tips', $tips); + $output .= '</div>'; } - $text = preg_replace('/\Won[a-z]+\s*=[^>]+?>/i', '>', $text); + return theme('form_element', t('Input format'), $output, $extra, $name, _form_get_error($name)); } + else { + // Only one format available: use a hidden form item and only show tips. + $format = array_shift($formats); + $output = form_hidden($name, $format->format); + $tips = _filter_tips(variable_get('filter_default_format', 0), false); + $output .= form_item(t('Formatting guidelines'), theme('filter_tips', $tips, $extra), $extra); + return $output; + } +} - if (variable_get('filter_html', FILTER_HTML_DONOTHING) == FILTER_HTML_ESCAPE) { - // Escape HTML - $text = htmlspecialchars($text); +/** + * Returns true if the user is allowed to access this format. + */ +function filter_access($format) { + if (user_access('administer filters')) { + return true; } + else { + $formats = filter_formats(); + return isset($formats[$format]); + } +} +/* @} */ - return trim($text); +/** + * Menu callback: show page with long filter tips + */ +function filter_tips_long() { + $format = arg(2); + if ($format) { + $output = theme('filter_tips', _filter_tips($format, true)); + } + else { + $output = theme('filter_tips', _filter_tips(-1, true)); + } + print theme('page', $output, t('Compose Tips')); +} + +/** + * Helper function for fetching filter tips. + */ +function _filter_tips($format, $long = false) { + if ($format == -1) { + $formats = filter_formats(); + } + else { + $formats = array(db_fetch_object(db_query("SELECT * FROM {filter_formats} WHERE format = %d", $format))); + } + + $tips = array(); + + foreach ($formats as $format) { + $filters = filter_list_format($format->format); + + $tips[$format->name] = array(); + foreach ($filters as $id => $filter) { + if ($tip = module_invoke($filter->module, 'filter_tips', $filter->delta, $format->format, $long)) { + $tips[$format->name][] = array('tip' => $tip, 'id' => $id); + } + } + } + + return $tips; } /** - * Settings for the filter system's built-in HTML handling. + * @addtogroup themeable + * @{ */ -function filter_default_settings() { - $group = form_radios(t('Filter HTML tags'), 'filter_html', variable_get('filter_html', FILTER_HTML_DONOTHING), array(FILTER_HTML_DONOTHING => t('Do not filter'), FILTER_HTML_STRIP => t('Strip tags'), FILTER_HTML_ESCAPE => t('Escape tags')), t('How to deal with HTML and PHP tags in user-contributed content. If set to "Strip tags", dangerous tags are removed (see below). If set to "Escape tags", all HTML is escaped and presented as it was typed.')); - $group .= form_textfield(t('Allowed HTML tags'), 'allowed_html', variable_get('allowed_html', '<a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul>'), 64, 255, t('If "Strip tags" is selected, optionally specify tags which should not be stripped. "ON*" attributes are always stripped.')); - $group .= form_radios(t('HTML style attributes'), 'filter_style', variable_get('filter_style', FILTER_STYLE_STRIP), array(FILTER_STYLE_ALLOW => t('Allowed'), FILTER_STYLE_STRIP => t('Removed')), t('If "Strip tags" is selected, you can choose whether "STYLE" attributes are allowed or removed from input.')); - $output .= form_group(t('HTML filtering'), $group); + +/** + * Format filter tips + * + * @param $tips + * @param $long + * @param $extra + */ +function theme_filter_tips($tips, $long = false, $extra = '') { + $output = ''; + + $multiple = count($tips) > 1; + if ($multiple) { + $output = t('Input formats') .':'; + } + + if (count($tips)) { + if ($multiple) { + $output .= '<ul>'; + } + foreach ($tips as $name => $tiplist) { + if ($multiple) { + $output .= '<li>'; + $output .= '<strong>'. $name .'</strong>:<br />'; + } + + $output .= '<ul class="tips">'; + + foreach ($tiplist as $tip) { + $output .= '<li'. ($long ? ' id="'. $tip['id'] .'">' : '>') . $tip['tip'] . '</li>'; + } + + $output .= '</ul>'; + if ($multiple) { + $output .= '</li>'; + } + } + if ($multiple) { + $output .= '</ul>'; + } + } return $output; } +/** @} End of addtogroup themeable */ + /** - * Implementation of hook_filter(). Handles URL upgrades from Drupal 4.1. + * @name Standard filters + * + * Filters implemented by the filter.module. + * @{ */ -function filter_filter($op, $text = '') { + +/** + * Implementation of hook_filter(). Contains a basic set of essential filters: + * - HTML filter: transform/validate user-supplied HTML + * - PHP evaluator: execute PHP code + * - Legacy filter: handle URL upgrades from Drupal 4.1. + * - Line break convertor: convert newlines into paragraph and break tags + */ +function filter_filter($op, $delta = 0, $format = -1, $text = '') { switch ($op) { - case 'name': - return t('Legacy filtering'); + case 'list': + return array(0 => t('HTML filter'), 1 => t('PHP evaluator'), 2 => t('Legacy filter'), 3 => t('Line break convertor')); + + case 'no cache': + return $delta == 1; // No caching for the PHP evaluator + + case 'description': + switch ($delta) { + case 0: + return t('Allows you to restrict if users can post HTML and which tags to filter out.'); + case 1: + return t('Runs a piece of PHP code. The usage of this filter should be restricted to administrators only!'); + case 2: + return t('Replaces URLs from Drupal 4.1 (and lower) with updated equivalents.'); + case 3: + return t('Converts line breaks into HTML (i.e. <br> and <p> tags).'); + default: + return; + } + case 'process': - if (variable_get('rewrite_old_urls', 0)) { - $text = filter_old_urls($text); + switch ($delta) { + case 0: + return _filter_html($text, $format); + case 1: + return drupal_eval($text); + case 2: + return _filter_old_urls($text, $format); + case 3: + return _filter_autop($text, $format); + default: + return $text; } - return $text; + case 'settings': - $group = form_radios(t('Rewrite old URLs'), 'rewrite_old_urls', variable_get('rewrite_old_urls', 0), array(t('Disabled'), t('Enabled')), t('The introduction of "clean URLs" in Drupal 4.2.0 breaks internal URLs that date back from Drupal 4.1.0 and before. If enabled, this filter will attempt to rewrite the old style URLs to avoid broken links. If <code>mod_rewrite</code> is available on your system, use the rewrite rules in Drupal\'s <code>.htaccess</code> file instead as these will also correct external referrers.')); - $output .= form_group(t('Legacy filtering'), $group); - return $output; + switch ($delta) { + case 0: + return _filter_html_settings($format); + default: + return; + } + default: return $text; } } +/** + * Settings for the HTML filter + */ +function _filter_html_settings($format) { + $group = form_radios(t('Filter HTML tags'), "filter_html_$format", variable_get("filter_html_$format", FILTER_HTML_STRIP), array(FILTER_HTML_STRIP => t('Strip tags'), FILTER_HTML_ESCAPE => t('Escape tags')), t('How to deal with HTML tags in user-contributed content. If set to "Strip tags", dangerous tags are removed (see below). If set to "Escape tags", all HTML is escaped and presented as it was typed.')); + $group .= form_textfield(t('Allowed HTML tags'), "allowed_html_$format", variable_get("allowed_html_$format", '<a> <b> <dd> <dl> <dt> <i> <li> <ol> <u> <ul>'), 64, 255, t('If "Strip tags" is selected, optionally specify tags which should not be stripped. Javascript event attributes are always stripped.')); + $group .= form_radios(t('HTML style attributes'), "filter_style_$format", variable_get("filter_style_$format", FILTER_STYLE_STRIP), array(FILTER_STYLE_ALLOW => t('Allowed'), FILTER_STYLE_STRIP => t('Removed')), t('If "Strip tags" is selected, you can choose whether "STYLE" attributes are allowed or removed from input.')); + $output .= form_group(t('HTML filter'), $group); + + return $output; +} + +/** + * HTML filter: provides filtering of input into accepted HTML + */ +function _filter_html($text, $format) { + if (variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_STRIP) { + // Allow users to enter HTML, but filter it + $text = strip_tags($text, variable_get("allowed_html_$format", '')); + if (variable_get("filter_style_$format", FILTER_STYLE_STRIP)) { + $text = preg_replace('/\Wstyle\s*=[^>]+?>/i', '>', $text); + } + $text = preg_replace('/\Won[a-z]+\s*=[^>]+?>/i', '>', $text); + } + + if (variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_ESCAPE) { + // Escape HTML + $text = htmlspecialchars($text); + } + + if (variable_get("filter_nl2br_$format", true)) { + $text = _filter_autop($text, true); + } + + return trim($text); +} + /** * Rewrite legacy URLs. * @@ -258,7 +870,11 @@ function filter_filter($op, $text = '') { * enough, we will use them to permanently rewrite the links in node * and comment bodies. */ -function filter_old_urls($text) { +function _filter_old_urls($text) { + if (!variable_get('rewrite_old_urls', 0)) { + return $text; + } + global $base_url; $end = substr($base_url, 12); @@ -310,31 +926,30 @@ function filter_old_urls($text) { } /** - * Fetch full filter help texts defined by modules. + * Convert line breaks into <p> and <br> in an intelligent fashion. + * From: http://photomatt.net/scripts/autop */ -function filter_tips_long() { - $tiplist = ''; - foreach (module_list() as $name) { - if ($tip = module_invoke($name, 'help', 'filter#long-tip')) { - $tiplist .= "<li id=\"filter-$name\">$tip</li>\n"; - } - } - $output = "<ul class=\"filter-tips-long\">\n$tiplist\n</ul>\n"; - print theme('page', $output, t('Compose Tips')); -} +function _filter_autop($text, $br = 1) { + $text = preg_replace('|\n*$|', '', $text) ."\n\n"; // just to make things a little easier, pad the end + $text = preg_replace('|<br />\s*<br />|', "\n\n", $text); + $text = preg_replace('!(<(?:table|ul|ol|li|pre|form|blockquote|h[1-6])[^>]*>)!', "\n$1", $text); // Space things out a little + $text = preg_replace('!(</(?:table|ul|ol|li|pre|form|blockquote|h[1-6])>)!', "$1\n", $text); // Space things out a little + $text = preg_replace("/\n\n+/", "\n\n", $text); // take care of duplicates + $text = preg_replace('/\n?(.+?)(?:\n\s*\n|\z)/s', "\t<p>$1</p>\n", $text); // make paragraphs, including one at the end + $text = preg_replace('|<p>\s*?</p>|', '', $text); // under certain strange conditions it could create a P of entirely whitespace + $text = preg_replace("|<p>(<li.+?)</p>|", "$1", $text); // problem with nested lists + $text = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $text); + $text = str_replace('</blockquote></p>', '</p></blockquote>', $text); + $text = preg_replace('!<p>\s*(</?(?:table|tr|td|th|div|ul|ol|li|pre|select|form|blockquote|p|h[1-6])[^>]*>)!', "$1", $text); + $text = preg_replace('!(</?(?:table|tr|td|th|div|ul|ol|li|pre|select|form|blockquote|p|h[1-6])[^>]*>)\s*</p>!', "$1", $text); + if ($br) $text = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $text); // optionally make line breaks + $text = preg_replace('!(</?(?:table|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|form|blockquote|p|h[1-6])[^>]*>)\s*<br />!', "$1", $text); + $text = preg_replace('!<br />(\s*</?(?:p|li|div|th|pre|td|ul|ol)>)!', '$1', $text); + $text = preg_replace('/&([^#])(?![a-z]{1,8};)/', '&$1', $text); -/** - * Fetch abbreviated filter help texts defined by modules. - */ -function filter_tips_short() { - $tiplist = ''; - foreach (module_list() as $name) { - if ($tip = module_invoke($name, 'help', 'filter#short-tip')) { - $tiplist .= "<li>$tip</li>\n"; - } - } - $tiplist .= '<li class="more-tips">' . l(t('More information on formatting options'), 'filter/tips') . '</li>'; - return "<ul class=\"filter-tips-short\">\n$tiplist\n</ul>\n"; + return $text; } +/* @} */ + ?> diff --git a/modules/forum.module b/modules/forum.module index d20ccb5cfc6a18920d0bfbdea420a270e36f22b3..017e5e8132b635d8510c1f2e1fe65ca171874c35 100644 --- a/modules/forum.module +++ b/modules/forum.module @@ -268,7 +268,7 @@ function forum_form(&$node) { $output .= form_checkbox(t('Leave shadow copy'), 'shadow', 1, $node->shadow, t('If you move this topic, you can leave a link in the old forum to the new forum.')); } - $output .= form_textarea(t('Body'), 'body', $node->body, 60, 10, filter_tips_short()); + $output .= form_textarea(t('Body'), 'body', $node->body, 60, 10, ''); return $output; } diff --git a/modules/forum/forum.module b/modules/forum/forum.module index d20ccb5cfc6a18920d0bfbdea420a270e36f22b3..017e5e8132b635d8510c1f2e1fe65ca171874c35 100644 --- a/modules/forum/forum.module +++ b/modules/forum/forum.module @@ -268,7 +268,7 @@ function forum_form(&$node) { $output .= form_checkbox(t('Leave shadow copy'), 'shadow', 1, $node->shadow, t('If you move this topic, you can leave a link in the old forum to the new forum.')); } - $output .= form_textarea(t('Body'), 'body', $node->body, 60, 10, filter_tips_short()); + $output .= form_textarea(t('Body'), 'body', $node->body, 60, 10, ''); return $output; } diff --git a/modules/node.module b/modules/node.module index 1a35d6c74e374e195c5b49ef49874b8983af962a..e01353ec6c2f54287d2ac04b350a155cf12c6534 100644 --- a/modules/node.module +++ b/modules/node.module @@ -168,6 +168,11 @@ function node_teaser($body) { return $body; } + // If the body contains PHP code, do not split it up to prevent parse errors. + if (strpos($body, '<?') != false) { + return $body; + } + // If a valid delimiter has been specified, use it to chop of the teaser. if ($delimiter > 0) { return substr($body, 0, $delimiter); @@ -511,10 +516,10 @@ function node_view($node, $teaser = FALSE, $page = FALSE) { function node_prepare($node, $teaser = FALSE) { $node->readmore = (strlen($node->teaser) < strlen($node->body)); if ($teaser == FALSE) { - $node->body = check_output($node->body); + $node->body = check_output($node->body, $node->format); } else { - $node->teaser = check_output($node->teaser); + $node->teaser = check_output($node->teaser, $node->format); } return $node; } @@ -999,7 +1004,7 @@ function node_feed($nodes = 0, $channel = array()) { // Load the specified node: $item = node_load(array('nid' => $node->nid)); $link = url("node/$node->nid", NULL, NULL, 1); - $items .= format_rss_item($item->title, $link, ($item->teaser ? $item->teaser : $item->body), array('pubDate' => date('r', $item->changed))); + $items .= format_rss_item($item->title, $link, check_output($item->teaser ? $item->teaser : $item->body, $item->format), array('pubDate' => date('r', $item->changed))); } $channel_defaults = array( @@ -1096,6 +1101,11 @@ function node_validate($node) { node_invoke($node, 'validate'); node_invoke_nodeapi($node, 'validate'); + // Check input format access + if (!filter_access($node->format)) { + form_set_error('format', t('The supplied input format is invalid.')); + } + $node->validated = TRUE; return $node; @@ -1156,6 +1166,10 @@ function node_form($edit) { $output .= '<div class="standard">'; $output .= form_textfield(t('Title'), 'title', $edit->title, 60, 128, NULL, NULL, TRUE); + // Add filter format selector / filter tips + + $output .= filter_form('format', $edit->format); + // Add the node-type-specific fields. $output .= $form; @@ -1516,7 +1530,7 @@ function node_nodeapi(&$node, $op, $arg = 0) { $output[t('revision')] = form_checkbox('', "node_revision_$node->type", 1, variable_get("node_revision_$node->type", 0)); return $output; case 'fields': - return array('nid', 'uid', 'type', 'title', 'teaser', 'body', 'revisions', 'status', 'promote', 'moderate', 'sticky', 'created', 'changed'); + return array('nid', 'uid', 'type', 'title', 'teaser', 'body', 'revisions', 'status', 'promote', 'moderate', 'sticky', 'created', 'changed', 'format'); } } @@ -1568,13 +1582,18 @@ function node_nodeapi(&$node, $op, $arg = 0) { * TRUE if the operation may be performed. */ function node_access($op, $node = NULL) { + // Convert the node to an object if necessary: + $node = array2object($node); + + // If the node is in a restricted format, disallow editing. + if ($op == 'update' && !filter_access($node->format)) { + return FALSE; + } + if (user_access('administer nodes')) { return TRUE; } - // Convert the node to an object if necessary: - $node = array2object($node); - // Can't use node_invoke(), because the access hook takes the $op parameter // before the $node parameter. $access = module_invoke(node_get_module_name($node), 'access', $op, $node); diff --git a/modules/node/node.module b/modules/node/node.module index 1a35d6c74e374e195c5b49ef49874b8983af962a..e01353ec6c2f54287d2ac04b350a155cf12c6534 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -168,6 +168,11 @@ function node_teaser($body) { return $body; } + // If the body contains PHP code, do not split it up to prevent parse errors. + if (strpos($body, '<?') != false) { + return $body; + } + // If a valid delimiter has been specified, use it to chop of the teaser. if ($delimiter > 0) { return substr($body, 0, $delimiter); @@ -511,10 +516,10 @@ function node_view($node, $teaser = FALSE, $page = FALSE) { function node_prepare($node, $teaser = FALSE) { $node->readmore = (strlen($node->teaser) < strlen($node->body)); if ($teaser == FALSE) { - $node->body = check_output($node->body); + $node->body = check_output($node->body, $node->format); } else { - $node->teaser = check_output($node->teaser); + $node->teaser = check_output($node->teaser, $node->format); } return $node; } @@ -999,7 +1004,7 @@ function node_feed($nodes = 0, $channel = array()) { // Load the specified node: $item = node_load(array('nid' => $node->nid)); $link = url("node/$node->nid", NULL, NULL, 1); - $items .= format_rss_item($item->title, $link, ($item->teaser ? $item->teaser : $item->body), array('pubDate' => date('r', $item->changed))); + $items .= format_rss_item($item->title, $link, check_output($item->teaser ? $item->teaser : $item->body, $item->format), array('pubDate' => date('r', $item->changed))); } $channel_defaults = array( @@ -1096,6 +1101,11 @@ function node_validate($node) { node_invoke($node, 'validate'); node_invoke_nodeapi($node, 'validate'); + // Check input format access + if (!filter_access($node->format)) { + form_set_error('format', t('The supplied input format is invalid.')); + } + $node->validated = TRUE; return $node; @@ -1156,6 +1166,10 @@ function node_form($edit) { $output .= '<div class="standard">'; $output .= form_textfield(t('Title'), 'title', $edit->title, 60, 128, NULL, NULL, TRUE); + // Add filter format selector / filter tips + + $output .= filter_form('format', $edit->format); + // Add the node-type-specific fields. $output .= $form; @@ -1516,7 +1530,7 @@ function node_nodeapi(&$node, $op, $arg = 0) { $output[t('revision')] = form_checkbox('', "node_revision_$node->type", 1, variable_get("node_revision_$node->type", 0)); return $output; case 'fields': - return array('nid', 'uid', 'type', 'title', 'teaser', 'body', 'revisions', 'status', 'promote', 'moderate', 'sticky', 'created', 'changed'); + return array('nid', 'uid', 'type', 'title', 'teaser', 'body', 'revisions', 'status', 'promote', 'moderate', 'sticky', 'created', 'changed', 'format'); } } @@ -1568,13 +1582,18 @@ function node_nodeapi(&$node, $op, $arg = 0) { * TRUE if the operation may be performed. */ function node_access($op, $node = NULL) { + // Convert the node to an object if necessary: + $node = array2object($node); + + // If the node is in a restricted format, disallow editing. + if ($op == 'update' && !filter_access($node->format)) { + return FALSE; + } + if (user_access('administer nodes')) { return TRUE; } - // Convert the node to an object if necessary: - $node = array2object($node); - // Can't use node_invoke(), because the access hook takes the $op parameter // before the $node parameter. $access = module_invoke(node_get_module_name($node), 'access', $op, $node); diff --git a/modules/page.module b/modules/page.module index ac3f6d4fbaa473f810ff156b8e126ebb7ce64cb3..4d03361722ca164302e9071980b05f069a1bad45 100644 --- a/modules/page.module +++ b/modules/page.module @@ -9,7 +9,6 @@ function page_help($section) { case 'admin/help#page': return t(" <p>The page module is used when you want to create content that optionally inserts a link into your navigation system. You can also, however, create pages that don't have this link by skipping the link text field in the page form. At this time, not all themes support the link insertion behavior. Some themes, like xtemplate, provide alternative mechanisms for link creation. Pages are also unique in that they shortcut the typical lifecycle of user generated content (i.e. submit -> moderate -> post -> comment). </p> - <p>If you enable the <strong>create PHP content</strong> permission for a role, pages may consist of PHP code in addition to HTML and text.</p> <h3>User access permissions for pages</h3> <p><strong>create pages:</strong> Allows a role to create pages. They cannot edit or delete pages, even if they are the authors. You must enable this permission to in order for a role to create a page.</p> <p><strong>edit own pages:</strong> Allows a role to add/edit pages if they own the page. Use this permission if you want users to be able to edit and maintain their own pages.</p> @@ -56,14 +55,14 @@ function page_access($op, $node) { * Implementation of hook_insert(). */ function page_insert($node) { - db_query("INSERT INTO {page} (nid, format, link, description) VALUES (%d, %d, '%s', '%s')", $node->nid, $node->format, $node->link, $node->description); + db_query("INSERT INTO {page} (nid, link, description) VALUES (%d, '%s', '%s')", $node->nid, $node->link, $node->description); } /** * Implementation of hook_update(). */ function page_update($node) { - db_query("UPDATE {page} SET format = %d, link = '%s', description = '%s' WHERE nid = %d", $node->format, $node->link, $node->description, $node->nid); + db_query("UPDATE {page} SET link = '%s', description = '%s' WHERE nid = %d", $node->link, $node->description, $node->nid); } /** @@ -77,7 +76,7 @@ function page_delete(&$node) { * Implementation of hook_load(). */ function page_load($node) { - return db_fetch_object(db_query('SELECT format, link, description FROM {page} WHERE nid = %d', $node->nid)); + return db_fetch_object(db_query('SELECT link, description FROM {page} WHERE nid = %d', $node->nid)); } /** @@ -92,23 +91,9 @@ function page_menu() { /** * Implementation of hook_content(). - * - * If body is dynamic (using PHP code), the body will be generated. */ function page_content($node, $teaser = FALSE) { - if ($node->format == 1) { - // PHP type - ob_start(); - eval($node->body); - $node->body = ob_get_contents(); - ob_end_clean(); - $node->teaser = node_teaser($node->body); - $node->readmore = (strlen($node->teaser) < strlen($node->body)); - } - else { - // Assume HTML type by default - $node = node_prepare($node, $teaser); - } + $node = node_prepare($node, $teaser); return $node; } @@ -128,38 +113,12 @@ function page_form(&$node) { $output .= implode('', taxonomy_node_form('page', $node)); } - if (($node->format == 1) && (!user_access('create php content'))) { - drupal_set_message(t('the body contents of this page are written in PHP and you do not have sufficient permissions to make changes to the body. You can edit the other elements of the page.')); - $output .= form_hidden('format', $node->format); - $hide_types = true; - } - else { - $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, filter_tips_short(), NULL, TRUE); - } + $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE); $output .= form_textfield(t('Link name'), 'link', $node->link, 60, 64, t('To make the page show up in the navigation links, enter the name of the link. Otherwise, leave this blank.')); $output .= form_textfield(t('Link description'), 'description', $node->description, 60, 64, t("The description displayed when hovering over the page's link. Leave blank when you don't want a description.")); - $content_type = (user_access('create php content')) ? array(0 => 'HTML', 1 => 'PHP') : false; - if (!$hide_types && $content_type) { - $output .= form_radios(t('Type'), 'format', $node->format, $content_type); - } return $output; } -/** - * Implementation of hook_validate(). - */ -function page_validate(&$node) { - if ($node->format && user_access('create php content')) { - // Do not filter PHP code, do not auto-extract a teaser - $node->teaser = $node->body; - } - - if (($node->format == 1) && (!user_access('create php content'))) { - /* Overwrite the submitted node body since they don't have sufficient privileges. */ - $node->body = db_result(db_query('SELECT body FROM {node} WHERE nid = %d', $node->nid)); - } -} - ?> diff --git a/modules/page/page.module b/modules/page/page.module index ac3f6d4fbaa473f810ff156b8e126ebb7ce64cb3..4d03361722ca164302e9071980b05f069a1bad45 100644 --- a/modules/page/page.module +++ b/modules/page/page.module @@ -9,7 +9,6 @@ function page_help($section) { case 'admin/help#page': return t(" <p>The page module is used when you want to create content that optionally inserts a link into your navigation system. You can also, however, create pages that don't have this link by skipping the link text field in the page form. At this time, not all themes support the link insertion behavior. Some themes, like xtemplate, provide alternative mechanisms for link creation. Pages are also unique in that they shortcut the typical lifecycle of user generated content (i.e. submit -> moderate -> post -> comment). </p> - <p>If you enable the <strong>create PHP content</strong> permission for a role, pages may consist of PHP code in addition to HTML and text.</p> <h3>User access permissions for pages</h3> <p><strong>create pages:</strong> Allows a role to create pages. They cannot edit or delete pages, even if they are the authors. You must enable this permission to in order for a role to create a page.</p> <p><strong>edit own pages:</strong> Allows a role to add/edit pages if they own the page. Use this permission if you want users to be able to edit and maintain their own pages.</p> @@ -56,14 +55,14 @@ function page_access($op, $node) { * Implementation of hook_insert(). */ function page_insert($node) { - db_query("INSERT INTO {page} (nid, format, link, description) VALUES (%d, %d, '%s', '%s')", $node->nid, $node->format, $node->link, $node->description); + db_query("INSERT INTO {page} (nid, link, description) VALUES (%d, '%s', '%s')", $node->nid, $node->link, $node->description); } /** * Implementation of hook_update(). */ function page_update($node) { - db_query("UPDATE {page} SET format = %d, link = '%s', description = '%s' WHERE nid = %d", $node->format, $node->link, $node->description, $node->nid); + db_query("UPDATE {page} SET link = '%s', description = '%s' WHERE nid = %d", $node->link, $node->description, $node->nid); } /** @@ -77,7 +76,7 @@ function page_delete(&$node) { * Implementation of hook_load(). */ function page_load($node) { - return db_fetch_object(db_query('SELECT format, link, description FROM {page} WHERE nid = %d', $node->nid)); + return db_fetch_object(db_query('SELECT link, description FROM {page} WHERE nid = %d', $node->nid)); } /** @@ -92,23 +91,9 @@ function page_menu() { /** * Implementation of hook_content(). - * - * If body is dynamic (using PHP code), the body will be generated. */ function page_content($node, $teaser = FALSE) { - if ($node->format == 1) { - // PHP type - ob_start(); - eval($node->body); - $node->body = ob_get_contents(); - ob_end_clean(); - $node->teaser = node_teaser($node->body); - $node->readmore = (strlen($node->teaser) < strlen($node->body)); - } - else { - // Assume HTML type by default - $node = node_prepare($node, $teaser); - } + $node = node_prepare($node, $teaser); return $node; } @@ -128,38 +113,12 @@ function page_form(&$node) { $output .= implode('', taxonomy_node_form('page', $node)); } - if (($node->format == 1) && (!user_access('create php content'))) { - drupal_set_message(t('the body contents of this page are written in PHP and you do not have sufficient permissions to make changes to the body. You can edit the other elements of the page.')); - $output .= form_hidden('format', $node->format); - $hide_types = true; - } - else { - $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, filter_tips_short(), NULL, TRUE); - } + $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE); $output .= form_textfield(t('Link name'), 'link', $node->link, 60, 64, t('To make the page show up in the navigation links, enter the name of the link. Otherwise, leave this blank.')); $output .= form_textfield(t('Link description'), 'description', $node->description, 60, 64, t("The description displayed when hovering over the page's link. Leave blank when you don't want a description.")); - $content_type = (user_access('create php content')) ? array(0 => 'HTML', 1 => 'PHP') : false; - if (!$hide_types && $content_type) { - $output .= form_radios(t('Type'), 'format', $node->format, $content_type); - } return $output; } -/** - * Implementation of hook_validate(). - */ -function page_validate(&$node) { - if ($node->format && user_access('create php content')) { - // Do not filter PHP code, do not auto-extract a teaser - $node->teaser = $node->body; - } - - if (($node->format == 1) && (!user_access('create php content'))) { - /* Overwrite the submitted node body since they don't have sufficient privileges. */ - $node->body = db_result(db_query('SELECT body FROM {node} WHERE nid = %d', $node->nid)); - } -} - ?> diff --git a/modules/statistics.module b/modules/statistics.module index 2d0d2937ceff663fbcffb49c5feea38f9ad9c0d0..846dbe86627d4233d0eabd6cf3813978e2bd7d3c 100644 --- a/modules/statistics.module +++ b/modules/statistics.module @@ -619,7 +619,7 @@ function statistics_summary($dbfield, $dbrows) { $links = link_node($content, 1); $output .= '<tr><td><strong>'. l($nid['title'], 'node/'. $nid['nid'], array('title' => t('View this posting.'))) .'</strong></td><td style="text-align: right;"><small>'. t('Submitted by %a on %b', array('%a' => format_name($content), '%b' => format_date($content->created, 'large'))) .'</small></td></tr>'; - $output .= '<tr><td colspan="2"><div style="margin-left: 20px;">'. check_output($content->teaser) .'</div></td></tr>'; + $output .= '<tr><td colspan="2"><div style="margin-left: 20px;">'. check_output($content->teaser, $content->format) .'</div></td></tr>'; $output .= '<tr><td style="text-align: right;" colspan="2">[ '. theme('links', $links) .' ]<br /><br /></td></tr>'; } diff --git a/modules/statistics/statistics.module b/modules/statistics/statistics.module index 2d0d2937ceff663fbcffb49c5feea38f9ad9c0d0..846dbe86627d4233d0eabd6cf3813978e2bd7d3c 100644 --- a/modules/statistics/statistics.module +++ b/modules/statistics/statistics.module @@ -619,7 +619,7 @@ function statistics_summary($dbfield, $dbrows) { $links = link_node($content, 1); $output .= '<tr><td><strong>'. l($nid['title'], 'node/'. $nid['nid'], array('title' => t('View this posting.'))) .'</strong></td><td style="text-align: right;"><small>'. t('Submitted by %a on %b', array('%a' => format_name($content), '%b' => format_date($content->created, 'large'))) .'</small></td></tr>'; - $output .= '<tr><td colspan="2"><div style="margin-left: 20px;">'. check_output($content->teaser) .'</div></td></tr>'; + $output .= '<tr><td colspan="2"><div style="margin-left: 20px;">'. check_output($content->teaser, $content->format) .'</div></td></tr>'; $output .= '<tr><td style="text-align: right;" colspan="2">[ '. theme('links', $links) .' ]<br /><br /></td></tr>'; } diff --git a/modules/story.module b/modules/story.module index 0efb933e383fcef1ca071a35e0cdcc48b3adcc2c..a29e2efc8b28d8122682567bef55d0f40dd6fed1 100644 --- a/modules/story.module +++ b/modules/story.module @@ -113,7 +113,7 @@ function story_form(&$node) { $output .= implode('', taxonomy_node_form('story', $node)); } - $output .= form_textarea(t('Body'), 'body', $node->body, 60, 15, filter_tips_short(), NULL, TRUE); + $output .= form_textarea(t('Body'), 'body', $node->body, 60, 15, '', NULL, TRUE); return $output; } diff --git a/modules/story/story.module b/modules/story/story.module index 0efb933e383fcef1ca071a35e0cdcc48b3adcc2c..a29e2efc8b28d8122682567bef55d0f40dd6fed1 100644 --- a/modules/story/story.module +++ b/modules/story/story.module @@ -113,7 +113,7 @@ function story_form(&$node) { $output .= implode('', taxonomy_node_form('story', $node)); } - $output .= form_textarea(t('Body'), 'body', $node->body, 60, 15, filter_tips_short(), NULL, TRUE); + $output .= form_textarea(t('Body'), 'body', $node->body, 60, 15, '', NULL, TRUE); return $output; } diff --git a/modules/system.module b/modules/system.module index 1fa3e93e575b1d5dd860747b1199e6d6e3579d7c..8187ae08fb0e673c1bc79ee398c6d387c098a4bc 100644 --- a/modules/system.module +++ b/modules/system.module @@ -43,7 +43,7 @@ function system_help_page() { * Implementation of hook_perm(). */ function system_perm() { - return array('administer site configuration', 'access administration pages', 'bypass input data check', 'create php content'); + return array('administer site configuration', 'access administration pages', 'bypass input data check'); } /** diff --git a/modules/system/system.module b/modules/system/system.module index 1fa3e93e575b1d5dd860747b1199e6d6e3579d7c..8187ae08fb0e673c1bc79ee398c6d387c098a4bc 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -43,7 +43,7 @@ function system_help_page() { * Implementation of hook_perm(). */ function system_perm() { - return array('administer site configuration', 'access administration pages', 'bypass input data check', 'create php content'); + return array('administer site configuration', 'access administration pages', 'bypass input data check'); } /**