wysiwyg_filter.admin.inc 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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. $default_value = wysiwyg_filter_array2csv($settings[$field_name]);
  107. $filterform[$field_name] = array(
  108. '#type' => 'textarea',
  109. '#title' => $rule_info['title'],
  110. '#default_value' => $default_value,
  111. '#cols' => 60,
  112. '#rows' => min(10, max(2, substr_count($default_value, "\n") + 2)),
  113. '#description' => $rule_info['description'],
  114. );
  115. // Display warning if the field is empty but the rule definition is not
  116. // complete.
  117. if (empty($default_value) && !_wysiwyg_filter_is_rule_definition_complete($rule_info, $valid_elements_parsed, $enabled_style_properties)) {
  118. drupal_set_message($rule_info['required_by_message'], 'warning');
  119. }
  120. }
  121. // *** Nofollow properties ***
  122. $filterform['nofollow'] = array(
  123. '#title' => t('Spam link deterrent settings'),
  124. '#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.'),
  125. );
  126. $filterform['nofollow_policy'] = array(
  127. '#type' => 'radios',
  128. '#title' => t('Policy'),
  129. '#options' => array(
  130. 'disabled' => t('Disabled - Do not add rel=&quot;nofollow&quot; to any link.'),
  131. 'whitelist' => t('Whitelist - Add rel=&quot;nofollow&quot; to all links except those leading to domain names specified in the list below.'),
  132. 'blacklist' => t('Blacklist - Add rel=&quot;nofollow&quot; to all links leading to domain names specified in the list below.'),
  133. ),
  134. '#default_value' => $settings['nofollow_policy'],
  135. '#description' => t('If you choose the whitelist option, be sure to add your own domain names to the list!'),
  136. );
  137. $parts = parse_url($base_url);
  138. // Note that domains list is stored by our submit handler in array form where
  139. // dots have been escaped, so we need here to revert the process to get a clean
  140. // string for user input where dots are unescaped.
  141. $nofollow_domains = wysiwyg_filter_array2csv($settings['nofollow_domains']);
  142. $filterform['nofollow_domains'] = array(
  143. '#type' => 'textarea',
  144. '#title' => t('Domains list'),
  145. '#default_value' => $nofollow_domains,
  146. '#cols' => 60,
  147. '#rows' => min(10, max(5, substr_count($nofollow_domains, "\n") + 2)),
  148. '#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.'),
  149. );
  150. return $filterform;
  151. }
  152. /*
  153. * Implements hook_form_FORM_ID_alter
  154. *
  155. * add validate and submit handlers
  156. */
  157. function wysiwyg_filter_form_filter_admin_format_form_alter(&$form, &$form_state, $form_id) {
  158. $form['#validate'][] = 'wysiwyg_filter_filter_wysiwyg_settings_validate';
  159. // Add the submit callback to the beginning of the array because we need
  160. // to prepare data for system_settings_form_submit().
  161. array_unshift($form['#submit'], 'wysiwyg_filter_filter_wysiwyg_settings_submit');
  162. }
  163. /**
  164. * Validate if the given rule definition is complete.
  165. *
  166. * @param $rule_info
  167. * An array of information about the rule we are about to check.
  168. * @param $elements
  169. * The array of all valid elements enabled for the current filter.
  170. * @param $style_properties
  171. * The array of all style properties enabled for the current filter.
  172. * @return
  173. * TRUE if the rule definiton is complete, FALSE otherwise.
  174. *
  175. * @see wysiwyg_filter_parse_valid_elements()
  176. * @see wysiwyg_filter_get_advanced_rules()
  177. */
  178. function _wysiwyg_filter_is_rule_definition_complete($rule_info, $elements, $style_properties) {
  179. foreach ($elements as $tag => $attributes) {
  180. if (isset($attributes[$rule_info['required_by']])) {
  181. // If this rule is not dependent on style properties, then we found it,
  182. // while we should not, so the rule is not complete.
  183. if (empty($rule_info['required_by_styles'])) {
  184. return FALSE;
  185. }
  186. // If this rule is dependent on style properties, then we need to check
  187. // if the related style properties exist.
  188. foreach ($rule_info['required_by_styles'] as $style_property) {
  189. if (isset($style_properties[$style_property])) {
  190. return FALSE;
  191. }
  192. }
  193. }
  194. }
  195. return TRUE;
  196. }
  197. /**
  198. * Clear any warning message we might have set previously.
  199. */
  200. function _wysiwyg_filter_clear_messages() {
  201. $messages = drupal_get_messages('warning');
  202. if (!empty($messages)) {
  203. foreach (wysiwyg_filter_get_advanced_rules() as $rule_info) {
  204. $my_messages[] = $rule_info['required_by_message'];
  205. }
  206. foreach ($messages['warning'] as $warning) {
  207. if (!in_array($warning, $my_messages)) {
  208. drupal_set_message($warning, 'warning');
  209. }
  210. }
  211. }
  212. }
  213. /**
  214. * Validate filter settings form.
  215. *
  216. * @ingroup forms
  217. */
  218. function wysiwyg_filter_filter_wysiwyg_settings_validate($form, &$form_state) {
  219. $values =& $form_state['values']['filters']['wysiwyg']['settings'];
  220. // Don't validate disabled filters.
  221. if (empty($form_state['values']['filters']['wysiwyg']['status'])) {
  222. return;
  223. }
  224. // *** validate valid_elements ***
  225. // Check elements against hardcoded backlist.
  226. $elements_blacklist = wysiwyg_filter_get_elements_blacklist();
  227. $valid_elements = trim($values['valid_elements']);
  228. $valid_elements = wysiwyg_filter_parse_valid_elements($valid_elements);
  229. $forbidden_elements = array();
  230. foreach (array_keys($valid_elements) as $element) {
  231. if (in_array($element, $elements_blacklist)) {
  232. $forbidden_elements[] = $element;
  233. }
  234. }
  235. if (!empty($forbidden_elements)) {
  236. form_set_error('valid_elements', t('The following elements cannot be allowed: %elements.', array('%elements' => implode(', ', $forbidden_elements))));
  237. }
  238. // *** validate nofollow_domains ***
  239. foreach (wysiwyg_filter_get_advanced_rules() as $rule_key => $rule_info) {
  240. $field_name = "rule_$rule_key";
  241. $expressions = array_filter(explode(',', preg_replace('#\s+#', ',', trim($values[$field_name])))); // form2db
  242. $errors = array();
  243. foreach ($expressions as $expression) {
  244. if (preg_match('`[*?]\*|\*\?`', $expression)) {
  245. $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));
  246. }
  247. if (!preg_match($rule_info['validate_regexp'], $expression)) {
  248. $errors[] = t('Invalid expression %expression. Please, check the syntax of the %field field.', array('%expression' => $expression, '%field' => $rule_info['title']));
  249. }
  250. }
  251. if (!empty($errors)) {
  252. form_set_error($field_name, implode('<br />', $errors));
  253. }
  254. }
  255. // *** validate nofollow_domains ***
  256. $nofollow_domains = array_filter(explode(',', preg_replace('#\s+#', ',', $values['nofollow_domains']))); // form2db
  257. foreach ($nofollow_domains as $nofollow_domain) {
  258. if (!preg_match('#^([a-z0-9]([-a-z0-9]*)?\.)+([a-z]+)$#i', $nofollow_domain)) {
  259. form_set_error('nofollow_domains', t('Invalid domain %domain. Please, enter a comma separated list of valid domain names.', array('%domain' => $nofollow_domain)));
  260. }
  261. }
  262. }
  263. /**
  264. * Submit processing for the filter settings form.
  265. *
  266. * Parse filter options to help us save resources that would otherwiese
  267. * require time and precious cpu cycles at filter processing time.
  268. *
  269. * @ingroup forms
  270. */
  271. function wysiwyg_filter_filter_wysiwyg_settings_submit($form, &$form_state) {
  272. $values =& $form_state['values']['filters']['wysiwyg']['settings'];
  273. // *** prepare valid_elements - just trim ***
  274. $values['valid_elements'] = trim($values['valid_elements']);
  275. // *** prepare rules - csv2array ***
  276. foreach (array_keys(wysiwyg_filter_get_advanced_rules()) as $rule_key) {
  277. $field_name = "rule_$rule_key";
  278. $values[$field_name] = wysiwyg_filter_csv2array($values[$field_name]);
  279. }
  280. // *** prepare nofollow_domains - csv2array ***
  281. $values['nofollow_domains'] = wysiwyg_filter_csv2array($values['nofollow_domains']);
  282. }
  283. /*
  284. * CSV to Array
  285. *
  286. * @param atring $v
  287. * @param bool $space2comma - shall we convet whitespace to commas before processing?
  288. * @return array
  289. */
  290. function wysiwyg_filter_csv2array($v, $space2comma = TRUE) {
  291. IF($space2comma) $v = preg_replace('#\s+#', ',', $v);
  292. return array_filter(explode(',', $v));
  293. }
  294. /*
  295. * Array to CSV
  296. *
  297. * @param array $v
  298. * @return string
  299. */
  300. function wysiwyg_filter_array2csv($v, $separator = ",\n") {
  301. return implode($separator, $v);
  302. }