wysiwyg_filter.admin.inc 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. <?php
  2. /**
  3. * @file
  4. * Administration pages for the WYSIWYG Filter module.
  5. */
  6. /**
  7. * Implements hook_filter_FILTER_settings
  8. *
  9. * @ingroup forms
  10. * TODO:
  11. * * remove defaults from info hook
  12. * * set defaults in helper func
  13. * * save settings for other formats in hidden form items
  14. * * remove format suffix, will be first array index
  15. * * care for pre- and post-processing, or remove it like parsed-elements
  16. * * rewrite validate to get correct values like $form['filters']['settings'][$name] / $form_state['values']['filters'][$name]['settings']
  17. *
  18. */
  19. function wysiwyg_filter_filter_wysiwyg_settings(&$form, &$form_state, $filter, $format, $defaults, $filters) {
  20. global $base_url;
  21. drupal_add_css(drupal_get_path('module', 'wysiwyg_filter') . '/wysiwyg_filter.admin.css', array('preprocess' => FALSE));
  22. // Load common functions.
  23. form_load_include($form_state, 'inc', 'wysiwyg_filter');
  24. $settings = $filter->settings;
  25. $settings += $defaults;
  26. // carry over settings for other formats
  27. $filterform = array();
  28. // *** valid elements ***
  29. $valid_elements = $settings['valid_elements'];
  30. $valid_elements_rows = min(20, max(5, substr_count($valid_elements, "\n") + 2));
  31. // show blacklisted elements in description
  32. $elements_blacklist = wysiwyg_filter_get_elements_blacklist();
  33. foreach ($elements_blacklist as $i => $element) {
  34. $elements_blacklist[$i] = '<' . $element . '>';
  35. }
  36. $filterform['valid_elements'] = array(
  37. '#type' => 'textarea',
  38. '#title' => t('HTML elements and attributes'),
  39. '#default_value' => $valid_elements,
  40. '#cols' => 60,
  41. '#rows' => $valid_elements_rows,
  42. '#description' => t('<p>
  43. This option allows you to specify which HTML elements and attributes are allowed in <a href="@valid-elements">TinyMCE valid_elements format</a>.
  44. </p>
  45. <strong>Syntax tips:</strong><ul>
  46. <li>Use a comma separated list to allow several HTML elements. Example: &quot;em,strong,br,p,ul,ol,li&quot;. Note that you can split your definitions using any number of lines.</li>
  47. <li>Use square brackets &quot;[]&quot; to specify the attributes that are allowed for each HTML element. Attributes should be whitelisted explicitly, otherwise element attributes will be ignored. Example: &quot;a&quot; will NOT allow users to post links, you should use &quot;a[href]&quot; instead!</li>
  48. <li>Use the vertical bar character &quot;|&quot; to separate several attribute definitions for a single HTML element. Example: &quot;a[href|target]&quot; means users may optionally specify the &quot;href&quot; and &quot;target&quot; attributes for &quot;a&quot; elements, any other attribute will be ignored.</li>
  49. <li>Use the exclamation mark &quot;!&quot; to set one attribute as being required for a particular HTML element. Example: &quot;a[!href|target]&quot; means users must specify the &quot;href&quot; attribute, otherwise the whole &quot;a&quot; element will be ignored. Users may optionally specify the &quot;target&quot; attribute as well. However, any other attribute will be ignored.</li>
  50. <li>Use the asterisk symbol &quot;*&quot; to whitelist all possible attributes for a particular HTML element. Example: &quot;a[*]&quot; means users will be allowed to use any attribute for the &quot;a&quot; element.</li>
  51. <li>Use the at sign character &quot;@&quot; to whitelist a common set of attributes for all allowed HTML elements. Example: &quot;@[class|style]&quot; means users will be allowed to use the &quot;class&quot; and &quot;style&quot; attributes for any whitelisted HTML element.</li>
  52. <li>For further information and examples, please consult documentation of the <a href="@valid-elements">valid_elements</a> option in the TinyMCE Wiki site.</li>
  53. </ul>
  54. <strong>Additional notes:</strong><ul>
  55. <li>The following elements cannot be whitelisted due to security reasons, to prevent users from breaking site layout and/or to avoid posting invalid HTML. Forbidden elements: %elements-blacklist.</li>
  56. <li>JavaScript event attributes such as onclick, onmouseover, etc. are always ignored. Should you need them, please consider using the &quot;Full HTML&quot; input format instead.</li>
  57. <li>If you allow usage of the attributes &quot;id&quot;, &quot;class&quot; and/or &quot;style&quot;, then you should also select which style properties are allowed and/or specify explicit matching rules for them using the &quot;Advanced rules&quot; section below.</li>
  58. </ul>', array(
  59. '@valid-elements' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Configuration/valid_elements',
  60. '%elements-blacklist' => implode(' ', $elements_blacklist),
  61. )),
  62. );
  63. // *** allow comments ***
  64. $filterform['allow_comments'] = array(
  65. '#type' => 'radios',
  66. '#title' => t('HTML comments'),
  67. '#options' => array(
  68. 0 => t('Disabled'),
  69. 1 => t('Enabled'),
  70. ),
  71. '#default_value' => $settings['allow_comments'],
  72. '#description' => t('Use this option to allow HTML comments.'),
  73. );
  74. // *** Style properties ***
  75. $filterform['styles'] = array(
  76. '#title' => t('Style properties'),
  77. '#description' => '<p>' . t('This section allows you to select which style properties can be used for HTML elements where the &quot;style&quot; attribute has been allowed. The <em>WYSIWYG Filter</em> will strip out style properties (and their values) not explicitly enabled here. On the other hand, for allowed style properties the <em>WYSIWYG Filter</em> will check their values for strict CSS syntax and strip out those that do not match.') . '</p>' .
  78. '<p>' . t('Additional matching rules should be specified from the &quot;Advanced rules&quot; section below for a few of these properties that may contain URLs in their values (&quot;background&quot;, &quot;background-image&quot;, &quot;list-style&quot; and &quot;list-style-image&quot;). Otherwise, these style properties will be ignored from user input.') . '</p>',
  79. );
  80. $style_property_groups = wysiwyg_filter_get_style_property_groups();
  81. $enabled_style_properties = array();
  82. $i = 0;
  83. foreach ($style_property_groups as $group => $group_info) {
  84. $style_properties = $settings["style_$group"];
  85. $enabled_style_properties += array_filter($style_properties);
  86. $filterform["style_$group"] = array(
  87. '#type' => 'checkboxes',
  88. '#title' => $group_info['title'],
  89. '#default_value' => $style_properties,
  90. '#options' => drupal_map_assoc(array_keys($group_info['properties'])),
  91. '#checkall' => TRUE,
  92. '#prefix' => '<div class="wysiwyg-filter-style-properties-group">',
  93. '#suffix' => '</div>'. (($i % 3) == 2?'<br style="clear:both"/>':''),
  94. );
  95. $i++;
  96. }
  97. // *** Advanced rules ***
  98. $filterform['rules'] = array(
  99. '#title' => t('Advanced rules'),
  100. '#prefix' => '<br style="clear:both"/>',
  101. '#description' => '<p>' . t('Use the following options to configure additional rules for certain HTML element attributes. As a safety measure, these rules should be defined explicitly. Otherwise, the corresponding HTML element attributes will be ignored from user input.') . '</p>',
  102. );
  103. $valid_elements_parsed = wysiwyg_filter_parse_valid_elements($settings['valid_elements']);
  104. foreach (wysiwyg_filter_get_advanced_rules() as $rule_key => $rule_info) {
  105. $field_name = "rule_$rule_key";
  106. $field_bypass_name = "rule_bypass_$rule_key";
  107. $default_value = wysiwyg_filter_array2csv($settings[$field_name]);
  108. $filterform[$field_name] = array(
  109. '#type' => 'textarea',
  110. '#title' => $rule_info['title'],
  111. '#default_value' => $default_value,
  112. '#cols' => 60,
  113. '#rows' => min(10, max(2, substr_count($default_value, "\n") + 2)),
  114. '#description' => $rule_info['description'],
  115. );
  116. $filterform[$field_bypass_name] = array(
  117. '#type' => 'checkbox',
  118. '#title' => t('Bypass %rule', array('%rule' => $rule_info['title'])),
  119. '#default_value' => !empty($settings[$field_bypass_name]),
  120. '#description' => t('Bypassing this rule may lead to security vulnerabilities. Only grant this filter to trusted roles.'),
  121. );
  122. // Display warning if the field is empty but the rule definition is not
  123. // complete.
  124. if (empty($settings[$field_bypass_name]) && empty($default_value) && !_wysiwyg_filter_is_rule_definition_complete($rule_info, $valid_elements_parsed, $enabled_style_properties)) {
  125. drupal_set_message($rule_info['required_by_message'], 'warning');
  126. }
  127. }
  128. // *** Nofollow properties ***
  129. $filterform['nofollow'] = array(
  130. '#title' => t('Spam link deterrent settings'),
  131. '#description' => t('As a measure to reduce the effectiveness of spam links, it is often recommended to add rel=&quot;nofollow&quot; to posted links leading to external sites. The WYSIWYG Filter can easily do this for you while HTML is being processed with almost no additional performance impact.'),
  132. );
  133. $filterform['nofollow_policy'] = array(
  134. '#type' => 'radios',
  135. '#title' => t('Policy'),
  136. '#options' => array(
  137. 'disabled' => t('Disabled - Do not add rel=&quot;nofollow&quot; to any link.'),
  138. 'whitelist' => t('Whitelist - Add rel=&quot;nofollow&quot; to all links except those leading to domain names specified in the list below.'),
  139. 'whitelist_current' => t('Whitelist - Add rel=&quot;nofollow&quot; to all links except those leading to current domain.'),
  140. 'blacklist' => t('Blacklist - Add rel=&quot;nofollow&quot; to all links leading to domain names specified in the list below.'),
  141. ),
  142. '#default_value' => $settings['nofollow_policy'],
  143. '#description' => t('If you choose the whitelist option, be sure to add your own domain names to the list!'),
  144. );
  145. $parts = parse_url($base_url);
  146. // Note that domains list is stored by our submit handler in array form where
  147. // dots have been escaped, so we need here to revert the process to get a clean
  148. // string for user input where dots are unescaped.
  149. $nofollow_domains = wysiwyg_filter_array2csv($settings['nofollow_domains']);
  150. $filterform['nofollow_domains'] = array(
  151. '#type' => 'textarea',
  152. '#title' => t('Domains list'),
  153. '#default_value' => $nofollow_domains,
  154. '#cols' => 60,
  155. '#rows' => min(10, max(5, substr_count($nofollow_domains, "\n") + 2)),
  156. '#description' => t('Enter a comma separated list of top level domain names. Note that all subdomains will also be included. Example: example.com will match example.com, www.example.com, etc.'),
  157. );
  158. return $filterform;
  159. }
  160. /*
  161. * Implements hook_form_FORM_ID_alter
  162. *
  163. * add validate and submit handlers
  164. */
  165. function wysiwyg_filter_form_filter_admin_format_form_alter(&$form, &$form_state, $form_id) {
  166. $form['#validate'][] = 'wysiwyg_filter_filter_wysiwyg_settings_validate';
  167. // Add the submit callback to the beginning of the array because we need
  168. // to prepare data for system_settings_form_submit().
  169. array_unshift($form['#submit'], 'wysiwyg_filter_filter_wysiwyg_settings_submit');
  170. }
  171. /**
  172. * Validate if the given rule definition is complete.
  173. *
  174. * @param $rule_info
  175. * An array of information about the rule we are about to check.
  176. * @param $elements
  177. * The array of all valid elements enabled for the current filter.
  178. * @param $style_properties
  179. * The array of all style properties enabled for the current filter.
  180. * @return
  181. * TRUE if the rule definiton is complete, FALSE otherwise.
  182. *
  183. * @see wysiwyg_filter_parse_valid_elements()
  184. * @see wysiwyg_filter_get_advanced_rules()
  185. */
  186. function _wysiwyg_filter_is_rule_definition_complete($rule_info, $elements, $style_properties) {
  187. foreach ($elements as $tag => $attributes) {
  188. if (isset($attributes[$rule_info['required_by']])) {
  189. // If this rule is not dependent on style properties, then we found it,
  190. // while we should not, so the rule is not complete.
  191. if (empty($rule_info['required_by_styles'])) {
  192. return FALSE;
  193. }
  194. // If this rule is dependent on style properties, then we need to check
  195. // if the related style properties exist.
  196. foreach ($rule_info['required_by_styles'] as $style_property) {
  197. if (isset($style_properties[$style_property])) {
  198. return FALSE;
  199. }
  200. }
  201. }
  202. }
  203. return TRUE;
  204. }
  205. /**
  206. * Validate filter settings form.
  207. *
  208. * @ingroup forms
  209. */
  210. function wysiwyg_filter_filter_wysiwyg_settings_validate($form, &$form_state) {
  211. $values =& $form_state['values']['filters']['wysiwyg']['settings'];
  212. // Don't validate disabled filters.
  213. if (empty($form_state['values']['filters']['wysiwyg']['status'])) {
  214. return;
  215. }
  216. // *** validate valid_elements ***
  217. // Check elements against hardcoded backlist.
  218. $elements_blacklist = wysiwyg_filter_get_elements_blacklist();
  219. $valid_elements = trim($values['valid_elements']);
  220. $valid_elements = wysiwyg_filter_parse_valid_elements($valid_elements);
  221. $forbidden_elements = array();
  222. foreach (array_keys($valid_elements) as $element) {
  223. if (in_array($element, $elements_blacklist)) {
  224. $forbidden_elements[] = $element;
  225. }
  226. }
  227. if (!empty($forbidden_elements)) {
  228. form_set_error('valid_elements', t('The following elements cannot be allowed: %elements.', array('%elements' => implode(', ', $forbidden_elements))));
  229. }
  230. // *** validate advanced rules ***
  231. foreach (wysiwyg_filter_get_advanced_rules() as $rule_key => $rule_info) {
  232. if (empty($settings["rule_bypass_$rule_key"])) {
  233. $field_name = "rule_$rule_key";
  234. $expressions = array_filter(explode(',', preg_replace('#\s+#', ',', trim($values[$field_name])))); // form2db
  235. $errors = array();
  236. foreach ($expressions as $expression) {
  237. if (preg_match('`[*?]\*|\*\?`', $expression)) {
  238. $errors[] = t('Invalid expression %expression. Please, do not use more than one consecutive asterisk (**) or one that is next to a question mark wildcard (?* or *?).', array('%expression' => $expression));
  239. }
  240. if (!preg_match($rule_info['validate_regexp'], $expression)) {
  241. $errors[] = t('Invalid expression %expression. Please, check the syntax of the %field field.', array('%expression' => $expression, '%field' => $rule_info['title']));
  242. }
  243. }
  244. if (!empty($errors)) {
  245. form_set_error($field_name, implode('<br />', $errors));
  246. }
  247. }
  248. }
  249. // *** validate nofollow_domains ***
  250. $nofollow_domains = array_filter(explode(',', preg_replace('#\s+#', ',', $values['nofollow_domains']))); // form2db
  251. foreach ($nofollow_domains as $nofollow_domain) {
  252. if (!preg_match('#^([a-z0-9]([-a-z0-9]*)?\.)+([a-z]+)$#i', $nofollow_domain)) {
  253. form_set_error('nofollow_domains', t('Invalid domain %domain. Please, enter a comma separated list of valid domain names.', array('%domain' => $nofollow_domain)));
  254. }
  255. }
  256. }
  257. /**
  258. * Submit processing for the filter settings form.
  259. *
  260. * Parse filter options to help us save resources that would otherwiese
  261. * require time and precious cpu cycles at filter processing time.
  262. *
  263. * @ingroup forms
  264. */
  265. function wysiwyg_filter_filter_wysiwyg_settings_submit($form, &$form_state) {
  266. $values =& $form_state['values']['filters']['wysiwyg']['settings'];
  267. // *** prepare valid_elements - just trim ***
  268. $values['valid_elements'] = trim($values['valid_elements']);
  269. // *** prepare rules - csv2array ***
  270. foreach (array_keys(wysiwyg_filter_get_advanced_rules()) as $rule_key) {
  271. $field_name = "rule_$rule_key";
  272. $values[$field_name] = wysiwyg_filter_csv2array($values[$field_name]);
  273. }
  274. // *** prepare nofollow_domains - csv2array ***
  275. $values['nofollow_domains'] = wysiwyg_filter_csv2array($values['nofollow_domains']);
  276. }
  277. /*
  278. * CSV to Array
  279. *
  280. * @param atring $v
  281. * @param bool $space2comma - shall we convet whitespace to commas before processing?
  282. * @return array
  283. */
  284. function wysiwyg_filter_csv2array($v, $space2comma = TRUE) {
  285. IF($space2comma) $v = preg_replace('#\s+#', ',', $v);
  286. return array_filter(explode(',', $v));
  287. }
  288. /*
  289. * Array to CSV
  290. *
  291. * @param array $v
  292. * @return string
  293. */
  294. function wysiwyg_filter_array2csv($v, $separator = ",\n") {
  295. return implode($separator, $v);
  296. }