ckeditor.inc 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. <?php
  2. /**
  3. * @file
  4. * Editor integration functions for CKEditor.
  5. */
  6. define('WYSIWYG_CKEDITOR_ACF_DISABLED', 0);
  7. define('WYSIWYG_CKEDITOR_ACF_AUTOMATIC', 1);
  8. define('WYSIWYG_CKEDITOR_ACF_CUSTOM', 2);
  9. /**
  10. * Plugin implementation of hook_editor().
  11. */
  12. function wysiwyg_ckeditor_editor() {
  13. $editor['ckeditor'] = array(
  14. 'title' => 'CKEditor',
  15. 'vendor url' => 'http://ckeditor.com',
  16. 'download url' => 'http://ckeditor.com/download',
  17. 'libraries' => array(
  18. '' => array(
  19. 'title' => 'Default',
  20. 'files' => array(
  21. 'ckeditor.js' => array('preprocess' => FALSE),
  22. ),
  23. ),
  24. 'src' => array(
  25. 'title' => 'Source',
  26. 'files' => array(
  27. 'ckeditor_source.js' => array('preprocess' => FALSE),
  28. ),
  29. ),
  30. ),
  31. 'install note callback' => 'wysiwyg_ckeditor_install_note',
  32. 'verified version range' => array('3.0', '4.6.1.580bcaf'),
  33. 'migrate settings callback' => 'wysiwyg_ckeditor_migrate_settings',
  34. 'version callback' => 'wysiwyg_ckeditor_version',
  35. 'themes callback' => 'wysiwyg_ckeditor_themes',
  36. 'settings form callback' => 'wysiwyg_ckeditor_settings_form',
  37. 'init callback' => 'wysiwyg_ckeditor_init',
  38. 'settings callback' => 'wysiwyg_ckeditor_settings',
  39. 'plugin callback' => '_wysiwyg_ckeditor_plugins',
  40. 'plugin meta callback' => '_wysiwyg_ckeditor_plugin_meta',
  41. 'proxy plugin' => array(
  42. 'drupal' => array(
  43. 'load' => TRUE,
  44. 'proxy' => TRUE,
  45. ),
  46. ),
  47. 'proxy plugin settings callback' => '_wysiwyg_ckeditor_proxy_plugin_settings',
  48. 'versions' => array(
  49. '3.0.0.3665' => array(
  50. 'js files' => array('ckeditor-3.0.js'),
  51. ),
  52. ),
  53. );
  54. return $editor;
  55. }
  56. /**
  57. * Profile migration callback for CKEditor.
  58. *
  59. * Applies known changes to the editor settings as needed when the installed
  60. * editor version is different from the one used to configure the profile.
  61. * This fixes problems caused by settings, plugins or buttons being renamed,
  62. * removed, or added between versions.
  63. *
  64. * Only changes needed up/down to and including the installed version from the
  65. * profile version may be applied, in case the user did not install the latest
  66. * supported version.
  67. *
  68. * @param $settings
  69. * The editor settings array as it was stored in the database.
  70. * @param $editor
  71. * The editor definition from wysiwyg_get_editor().
  72. * @param $profile_version
  73. * The editor version string from when the profile was last saved.
  74. * @param $installed_version
  75. * The editor version currently installed on the system.
  76. *
  77. * @return
  78. * An editor version string telling Wysiwyg past which version the profile
  79. * could be migrated. If no changes were needed return TRUE.
  80. * Returning FALSE indicates migration failed and the profile is likely
  81. * unusable. Wysiwyg will recommend the user starts over with a new profile.
  82. */
  83. function wysiwyg_ckeditor_migrate_settings(&$settings, $editor, $profile_version, $installed_version) {
  84. $version_diff = version_compare($installed_version, $profile_version);
  85. // Default to no changes needed.
  86. $migrated_version = TRUE;
  87. if ($version_diff === 1) {
  88. // Upgrading, starting at the profile version going up.
  89. // 3.x to 4.0.
  90. if (version_compare($profile_version, '4.0', '<')
  91. && version_compare($installed_version, '4.0', '>=')) {
  92. // The default skin changed from "kama" to "moono".
  93. if (isset($settings['skin']) && $settings['skin'] === 'kama') {
  94. $settings['skin'] = 'moono';
  95. }
  96. $migrated_version = '4.0';
  97. }
  98. // Version 4.6.0.
  99. if (version_compare($profile_version, '4.6.0', '<')
  100. && version_compare($installed_version, '4.6.0', '>=')) {
  101. // The default skin changed from "moono" to "moono-lisa".
  102. if (isset($settings['skin']) && $settings['skin'] === 'moono') {
  103. $settings['skin'] = 'moono-lisa';
  104. }
  105. $migrated_version = '4.6.0';
  106. }
  107. }
  108. elseif ($version_diff === 0) {
  109. // Same version. This function would never have been called.
  110. }
  111. // $version_diff === -1, an older version was installed.
  112. else {
  113. // Downgrading, starting at the profile version going down.
  114. // 4.6.0 down to 4.x.
  115. if (version_compare($profile_version, '4.6', '>=')
  116. && version_compare($installed_version, '4.6', '<')) {
  117. if (isset($settings['skin']) && $settings['skin'] === 'moono-lisa') {
  118. $settings['skin'] = 'moono';
  119. }
  120. // Going down directly to 4.0 since no changes need to run anyway.
  121. $migrated_version = '4.6';
  122. }
  123. // 4.x to 3.x.
  124. if (version_compare($profile_version, '4.0', '>=')
  125. && version_compare($installed_version, '4.0', '<')) {
  126. // Change the default skin back "moono" to "kama".
  127. if (isset($settings['skin']) && $settings['skin'] === 'moono') {
  128. $settings['skin'] = 'kama';
  129. }
  130. $migrated_version = '4.0';
  131. }
  132. }
  133. // Return the version which was possible to migrate to, or FALSE on fail. Must
  134. // be within the verified range, but not necessarily match the exact version
  135. // which is currently installed.
  136. return $migrated_version;
  137. }
  138. /**
  139. * Return an install note.
  140. */
  141. function wysiwyg_ckeditor_install_note() {
  142. $output = '<p class="warning">' . t('Do NOT download the "CKEditor for Drupal" edition.') . '</br>';
  143. $output .= t('Make sure you install the full package as not all plugins work with the standard package.') . '</p>';
  144. return $output;
  145. }
  146. /**
  147. * Detect editor version.
  148. *
  149. * @param $editor
  150. * An array containing editor properties as returned from hook_editor().
  151. *
  152. * @return
  153. * The installed editor version.
  154. */
  155. function wysiwyg_ckeditor_version($editor) {
  156. $library = $editor['library path'] . '/ckeditor.js';
  157. if (!file_exists($library)) {
  158. return;
  159. }
  160. $library = fopen($library, 'r');
  161. $max_lines = 8;
  162. while ($max_lines && $line = fgets($library, 500)) {
  163. // version:'CKEditor 3.0 SVN',revision:'3665'
  164. // version:'3.0 RC',revision:'3753'
  165. // version:'3.0.1',revision:'4391'
  166. // version:"4.0",revision:"769d96134b"
  167. if (preg_match('@version:[\'"](?:CKEditor )?([\d\.]+)(?:.+revision:[\'"]([[:xdigit:]]+))?@', $line, $version)) {
  168. fclose($library);
  169. // Version numbers need to have three parts since 3.0.1.
  170. $version[1] = preg_replace('/^(\d+)\.(\d+)$/', '${1}.${2}.0', $version[1]);
  171. return $version[1] . '.' . $version[2];
  172. }
  173. $max_lines--;
  174. }
  175. fclose($library);
  176. }
  177. /**
  178. * Determine available editor themes or check/reset a given one.
  179. *
  180. * @param $editor
  181. * A processed hook_editor() array of editor properties.
  182. * @param $profile
  183. * A wysiwyg editor profile.
  184. *
  185. * @return
  186. * An array of theme names. The first returned name should be the default
  187. * theme name.
  188. */
  189. function wysiwyg_ckeditor_themes($editor, $profile) {
  190. // @todo Skins are not themes but this will do for now.
  191. $path = $editor['library path'] . '/skins/';
  192. if (file_exists($path) && ($dir_handle = opendir($path))) {
  193. $themes = array();
  194. while ($file = readdir($dir_handle)) {
  195. if (is_dir($path . $file) && substr($file, 0, 1) != '.' && $file != 'CVS') {
  196. $themes[] = $file;
  197. }
  198. }
  199. closedir($dir_handle);
  200. natcasesort($themes);
  201. $themes = array_values($themes);
  202. return !empty($themes) ? $themes : array('default');
  203. }
  204. else {
  205. return array('default');
  206. }
  207. }
  208. /**
  209. * Enhances the editor profile settings form for CKEditor.
  210. *
  211. * @see http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.config.html
  212. */
  213. function wysiwyg_ckeditor_settings_form(&$form, &$form_state) {
  214. $profile = $form_state['wysiwyg_profile'];
  215. $settings = $profile->settings;
  216. $installed_version = $form_state['wysiwyg']['editor']['installed version'];
  217. $ckeditor_defaults = array(
  218. 'block_formats' => 'p,address,pre,h2,h3,h4,h5,h6,div',
  219. // Custom setting.
  220. 'default_toolbar_grouping' => FALSE,
  221. 'forcePasteAsPlainText' => FALSE,
  222. 'resize_enabled' => TRUE,
  223. 'simple_source_formatting' => FALSE,
  224. 'toolbarLocation' => 'top',
  225. 'allowedContent' => TRUE,
  226. );
  227. if (version_compare($installed_version, '3.1.0', '>=')) {
  228. // Enabled by default.
  229. $ckeditor_defaults['pasteFromWordRemoveFontStyles'] = TRUE;
  230. $ckeditor_defaults['pasteFromWordRemoveStyles'] = TRUE;
  231. }
  232. if (version_compare($installed_version, '4.6.0', '>=')) {
  233. // Disabled by default, deprecated.
  234. $ckeditor_defaults['pasteFromWordRemoveFontStyles'] = FALSE;
  235. // Dropped, no effect.
  236. unset($ckeditor_defaults['pasteFromWordNumberedHeadingToList'],
  237. $ckeditor_defaults['pasteFromWordRemoveStyles']);
  238. }
  239. if (version_compare($installed_version, '3.2.1', '>=')) {
  240. $ckeditor_defaults['stylesSet'] = '';
  241. }
  242. $settings += $ckeditor_defaults;
  243. $form['appearance']['toolbarLocation'] = array(
  244. '#type' => 'select',
  245. '#title' => t('Toolbar location'),
  246. '#default_value' => $settings['toolbarLocation'],
  247. '#options' => array('bottom' => t('Bottom'), 'top' => t('Top')),
  248. '#description' => t('This option controls whether the editor toolbar is displayed above or below the editing area.') . ' ' . t('Uses the <a href="@url">@setting</a> setting internally.', array('@setting' => 'toolbarLocation', '@url' => url('http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-toolbarLocation'))),
  249. );
  250. $form['appearance']['resize_enabled'] = array(
  251. '#type' => 'checkbox',
  252. '#title' => t('Enable resizing button'),
  253. '#default_value' => $settings['resize_enabled'],
  254. '#return_value' => 1,
  255. '#description' => t('This option gives you the ability to enable/disable the editor resizing feature.') . ' ' . t('Uses the <a href="@url">@setting</a> setting internally.', array('@setting' => 'resize_enabled', '@url' => url('http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-resize_enabled'))),
  256. );
  257. $form['output']['simple_source_formatting'] = array(
  258. '#type' => 'checkbox',
  259. '#title' => t('Apply simple source formatting'),
  260. '#default_value' => $settings['simple_source_formatting'],
  261. '#return_value' => 1,
  262. '#description' => t('If enabled, the editor will re-format the HTML source code using a simple set of predefined rules. Disabling this option could avoid conflicts with other input filters.') . ' ' . t('Uses the <a href="@url">@setting</a> setting internally.', array('@setting' => 'dataProcessor.write.setRules()', '@url' => url('http://docs.cksource.com/ckeditor_api/symbols/src/plugins_htmlwriter_plugin.js.html'))),
  263. );
  264. $form['paste'] = array(
  265. '#type' => 'fieldset',
  266. '#title' => t('Paste plugin'),
  267. '#description' => t('Settings for the <a href="@url">@plugin</a> plugin.', array('@plugin' => 'paste', '@url' => url('http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-forcePasteAsPlainText'))),
  268. '#collapsible' => TRUE,
  269. '#collapsed' => TRUE,
  270. '#group' => 'advanced',
  271. );
  272. $form['paste']['forcePasteAsPlainText'] = array(
  273. '#type' => 'checkbox',
  274. '#title' => t('Force paste as plain text'),
  275. '#default_value' => !empty($settings['forcePasteAsPlainText']),
  276. '#return_value' => 1,
  277. '#description' => t('If enabled, all pasting operations insert plain text into the editor, losing any formatting information possibly available in the source text. Note: Paste from Word is not affected by this setting.') . ' ' . t('Uses the <a href="@url">@setting</a> setting internally.', array('@setting' => 'forcePasteAsPlainText', '@url' => url('http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-forcePasteAsPlainText'))),
  278. );
  279. if (version_compare($installed_version, '3.1.0', '>=')) {
  280. $form['paste']['pasteFromWord'] = array(
  281. '#type' => 'fieldset',
  282. '#title' => t('Paste from Word'),
  283. );
  284. $form['paste']['pasteFromWord']['pasteFromWordNumberedHeadingToList'] = array(
  285. '#type' => 'checkbox',
  286. '#title' => t('Numbered heading to list'),
  287. '#default_value' => !empty($settings['pasteFromWordNumberedHeadingToList']),
  288. '#return_value' => 1,
  289. '#description' => t('If enabled, transforms MS Word outline numbered headings into lists.'),
  290. );
  291. $form['paste']['pasteFromWord']['pasteFromWordPromptCleanup'] = array(
  292. '#type' => 'checkbox',
  293. '#title' => t('Prompt on cleanup'),
  294. '#default_value' => !empty($settings['pasteFromWordPromptCleanup']),
  295. '#return_value' => 1,
  296. '#description' => t('If enabled, prompts the user about the clean up of content being pasted from MS Word.'),
  297. );
  298. $form['paste']['pasteFromWord']['pasteFromWordRemoveFontStyles'] = array(
  299. '#type' => 'checkbox',
  300. '#title' => t('Remove font styles'),
  301. '#default_value' => !empty($settings['pasteFromWordRemoveFontStyles']),
  302. '#return_value' => 1,
  303. '#description' => t('If enabled, removes all font related formatting styles, including font size, font family, font foreground/background color.'),
  304. );
  305. $form['paste']['pasteFromWord']['pasteFromWordRemoveStyles'] = array(
  306. '#type' => 'checkbox',
  307. '#title' => t('Remove styles'),
  308. '#default_value' => !empty($settings['pasteFromWordRemoveStyles']),
  309. '#return_value' => 1,
  310. '#description' => t('If enabled, removes element styles that can not be managed with the editor, other than font specific styles.'),
  311. );
  312. }
  313. if (version_compare($installed_version, '4.1.0', '>=')) {
  314. $form['output']['acf'] = array(
  315. '#type' => 'fieldset',
  316. '#title' => t('Advanced Content Filter'),
  317. '#description' => t('ACF limits and adapts input data (HTML code added in source mode or by the editor.setData method, pasted HTML code, etc.) so it matches the editor configuration in the best possible way. It may also deactivate features which generate HTML code that is not allowed by the configuration. See <a href="@url">@url</a> for details.', array('@url' => url('http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter'))),
  318. );
  319. $form['output']['acf']['acf_mode'] = array(
  320. '#type' => 'select',
  321. '#title' => t('Mode'),
  322. '#options' => array(
  323. WYSIWYG_CKEDITOR_ACF_AUTOMATIC => t('Automatic'),
  324. WYSIWYG_CKEDITOR_ACF_CUSTOM => t('Custom'),
  325. WYSIWYG_CKEDITOR_ACF_DISABLED => t('Disabled'),
  326. ),
  327. '#default_value' => isset($profile->settings['acf_mode']) ? $profile->settings['acf_mode'] : WYSIWYG_CKEDITOR_ACF_DISABLED,
  328. '#description' => t('If set to <em>Automatic</em> or <em>Custom</em>, the editor will strip out any content not explicitely allowed <strong>when the editor loads</strong>.'),
  329. );
  330. $form['output']['acf']['acf_allowed_content'] = array(
  331. '#type' => 'textarea',
  332. '#title' => t('Content Rules'),
  333. '#default_value' => isset($profile->settings['acf_allowed_content']) ? $profile->settings['acf_allowed_content'] : '',
  334. '#description' => t('Rules for whitelisting content for the advanced content filter. Both string and object formats accepted. Uses the <a href="@allowed_url">allowedContent</a> setting in <em>Custom</em> mode <strong>or</strong> the <a href="@extra_allwed_url">extraAllowedContent</a> settings in <em>Automatic</em> mode internally. See <a href="@info_url">@info_url</a> for details.', array('@info_url' => url('http://docs.ckeditor.com/#!/guide/dev_allowed_content_rules'), '@allowed_url' => url('http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-allowedContent'), '@allowed_extra_url' => url('http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-extraAllowedContent'))),
  335. '#states' => array(
  336. 'visible' => array(
  337. ':input[name="acf_mode"]' => array(
  338. array('value' => WYSIWYG_CKEDITOR_ACF_AUTOMATIC),
  339. array('value' => WYSIWYG_CKEDITOR_ACF_CUSTOM),
  340. ),
  341. ),
  342. ),
  343. '#element_validate' => array('wysiwyg_ckeditor_settings_form_validate_allowed_content'),
  344. );
  345. }
  346. if (version_compare($installed_version, '3.6.0', '>=')) {
  347. $form['appearance']['default_toolbar_grouping'] = array(
  348. '#type' => 'checkbox',
  349. '#title' => t('Use default toolbar button grouping'),
  350. '#default_value' => !empty($settings['default_toolbar_grouping']),
  351. '#return_value' => 1,
  352. '#description' => t('This option gives you the ability to enable/disable the usage of default groupings for toolbar buttons. If enabled, toolbar buttons will be placed into predetermined groups instead of all in a single group.'),
  353. );
  354. }
  355. if (version_compare($installed_version, '3.2.1', '>=')) {
  356. // Versions below 3.2.1 do not support Font styles at all.
  357. $form['css']['stylesSet'] = array(
  358. '#type' => 'textarea',
  359. '#title' => t('CSS classes'),
  360. '#description' => t('Optionally define CSS classes for the "Font style" dropdown list.<br />Enter one class on each line in the format: !format. Example: !example<br />If left blank, CSS classes are automatically imported from loaded stylesheet(s).', array(
  361. '@url' => url('http://docs.ckeditor.com/#!/api/CKEDITOR.stylesSet'),
  362. '!format' => '<code>[label]=[element].[class]</code>',
  363. '!example' => '<code>Title=h1.title</code>',
  364. )) . ' ' . t('Uses the <a href="@url">@setting</a> setting internally.', array('@setting' => 'stylesSet', '@url' => url('http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-stylesSet'))),
  365. '#default_value' => $settings['stylesSet'],
  366. '#element_validate' => array('wysiwyg_ckeditor_settings_form_validate_stylessets'),
  367. );
  368. }
  369. if (version_compare($installed_version, '4.6.0', '>=')) {
  370. $form['paste']['pasteFromWord']['pasteFromWordRemoveFontStyles']['#description'] .= '<br />' . t('This setting will be deprecated in the future. Use ACF to replicate the effect of enabling it.');
  371. unset($form['paste']['pasteFromWord']['pasteFromWordNumberedHeadingToList'],
  372. $form['paste']['pasteFromWord']['pasteFromWordRemoveStyles']);
  373. }
  374. $form['css']['block_formats'] = array(
  375. '#type' => 'textfield',
  376. '#title' => t('Block formats'),
  377. '#default_value' => $settings['block_formats'],
  378. '#size' => 40,
  379. '#maxlength' => 250,
  380. '#description' => t('Comma separated list of HTML block formats. Possible values: <code>@format-list</code>.', array('@format-list' => 'p,h1,h2,h3,h4,h5,h6,div,blockquote,address,pre,code,dt,dd and other block elements')) . ' ' . t('Uses the <a href="@url">@setting</a> setting internally.', array('@setting' => 'block_formats', '@url' => url('http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-format_tags'))),
  381. );
  382. }
  383. /**
  384. * #element_validate handler for ACF Allowed Content element altered by wysiwyg_ckeditor_settings_form().
  385. */
  386. function wysiwyg_ckeditor_settings_form_validate_allowed_content($element, &$form_state) {
  387. if (_wysiwyg_ckeditor_settings_acf_is_obj($element['#value']) && json_decode($element['#value']) === NULL) {
  388. form_error($element, t('Allowed content is not valid JSON.'));
  389. }
  390. }
  391. /**
  392. * #element_validate handler for CSS classes element altered by wysiwyg_ckeditor_settings_form().
  393. */
  394. function wysiwyg_ckeditor_settings_form_validate_stylessets($element, &$form_state) {
  395. if (wysiwyg_ckeditor_settings_parse_styles($element['#value']) === FALSE) {
  396. form_error($element, t('The specified CSS classes are syntactically incorrect.'));
  397. }
  398. }
  399. /**
  400. * Returns an initialization JavaScript for this editor library.
  401. *
  402. * @param array $editor
  403. * The editor library definition.
  404. * @param string $library
  405. * The library variant key from $editor['libraries'].
  406. * @param object $profile
  407. * The (first) wysiwyg editor profile.
  408. *
  409. * @return string
  410. * A string containing inline JavaScript to execute before the editor library
  411. * script is loaded.
  412. */
  413. function wysiwyg_ckeditor_init($editor) {
  414. // CKEditor unconditionally searches for its library filename in SCRIPT tags
  415. // on the page upon loading the library in order to determine the base path to
  416. // itself. When JavaScript aggregation is enabled, this search fails and all
  417. // relative constructed paths within CKEditor are broken. The library has a
  418. // CKEditor.basePath property, but it is not publicly documented and thus not
  419. // reliable. The official documentation suggests to solve the issue through
  420. // the global window variable.
  421. // @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Specifying_the_Editor_Path
  422. $library_path = base_path() . $editor['library path'] . '/';
  423. return <<<EOL
  424. window.CKEDITOR_BASEPATH = '$library_path';
  425. EOL;
  426. }
  427. /**
  428. * Return runtime editor settings for a given wysiwyg profile.
  429. *
  430. * @param $editor
  431. * A processed hook_editor() array of editor properties.
  432. * @param $config
  433. * An array containing wysiwyg editor profile settings.
  434. * @param $theme
  435. * The name of a theme/GUI/skin to use.
  436. *
  437. * @return
  438. * A settings array to be populated in
  439. * Drupal.settings.wysiwyg.configs.{editor}
  440. */
  441. function wysiwyg_ckeditor_settings($editor, $config, $theme) {
  442. $default_skin = (version_compare($editor['installed version'], '4.0.0', '<') ? 'kama' : (version_compare($editor['installed version'], '4.6.0', '<') ? 'moono' : 'moono-lisa'));
  443. $settings = array(
  444. // Needed to make relative paths work in the editor.
  445. 'baseHref' => $GLOBALS['base_url'] . '/',
  446. 'width' => 'auto',
  447. // For better compatibility with smaller textareas.
  448. 'resize_minWidth' => 450,
  449. // @todo Do not use skins as themes and add separate skin handling.
  450. 'theme' => 'default',
  451. 'skin' => !empty($theme) ? $theme : $default_skin,
  452. // By default, CKEditor converts most characters into HTML entities. Since
  453. // it does not support a custom definition, but Drupal supports Unicode, we
  454. // disable at least the additional character sets. CKEditor always converts
  455. // XML default characters '&', '<', '>'.
  456. // @todo Check whether completely disabling ProcessHTMLEntities is an option.
  457. 'entities_latin' => FALSE,
  458. 'entities_greek' => FALSE,
  459. );
  460. // Add HTML block format settings; common block formats are already predefined
  461. // by CKEditor.
  462. if (isset($config['block_formats'])) {
  463. $block_formats = explode(',', drupal_strtolower(preg_replace('@\s+@', '', $config['block_formats'])));
  464. $predefined_formats = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'address', 'div');
  465. foreach (array_diff($block_formats, $predefined_formats) as $tag) {
  466. $tag = trim($tag);
  467. $settings["format_$tag"] = array('element' => $tag, 'name' => strtoupper(substr($tag, 0, 1)) . substr($tag, 1));
  468. }
  469. $settings['format_tags'] = implode(';', $block_formats);
  470. }
  471. // Advanced Content Filter
  472. // @see http://docs.ckeditor.com/#!/guide/dev_advanced_content_filter
  473. if (!isset($config['acf_mode']) || WYSIWYG_CKEDITOR_ACF_DISABLED == $config['acf_mode']) {
  474. $settings['allowedContent'] = TRUE;
  475. }
  476. else {
  477. if (_wysiwyg_ckeditor_settings_acf_is_obj($config['acf_allowed_content'])) {
  478. $acf_content = json_decode($config['acf_allowed_content']);
  479. }
  480. else {
  481. $acf_content = $config['acf_allowed_content'];
  482. }
  483. if (WYSIWYG_CKEDITOR_ACF_CUSTOM == $config['acf_mode']) {
  484. $settings['allowedContent'] = $acf_content;
  485. }
  486. elseif (WYSIWYG_CKEDITOR_ACF_AUTOMATIC == $config['acf_mode']) {
  487. $settings['extraAllowedContent'] = $acf_content;
  488. }
  489. }
  490. if (isset($config['css_setting'])) {
  491. // Versions below 3.0.1 could only handle one stylesheet.
  492. if (version_compare($editor['installed version'], '3.0.1.4391', '<')) {
  493. if ($config['css_setting'] == 'theme') {
  494. $css = wysiwyg_get_css(isset($config['css_theme']) ? $config['css_theme'] : '');
  495. $settings['contentsCss'] = reset($css);
  496. }
  497. elseif ($config['css_setting'] == 'self' && isset($config['css_path'])) {
  498. $settings['contentsCss'] = strtr($config['css_path'], array(
  499. '%b' => base_path(),
  500. '%t' => drupal_get_path('theme', variable_get('theme_default', NULL)),
  501. '%q' => variable_get('css_js_query_string', ''),
  502. ));
  503. }
  504. }
  505. else {
  506. if ($config['css_setting'] == 'theme') {
  507. $settings['contentsCss'] = wysiwyg_get_css(isset($config['css_theme']) ? $config['css_theme'] : '');
  508. }
  509. elseif ($config['css_setting'] == 'self' && isset($config['css_path'])) {
  510. $settings['contentsCss'] = explode(',', strtr($config['css_path'], array(
  511. '%b' => base_path(),
  512. '%t' => drupal_get_path('theme', variable_get('theme_default', NULL)),
  513. '%q' => variable_get('css_js_query_string', ''),
  514. )));
  515. }
  516. }
  517. }
  518. // Parse and define the styles set for the Styles plugin (3.2.1+).
  519. // @todo This should be a plugin setting, but Wysiwyg does not support
  520. // plugin-specific settings yet.
  521. if (!empty($config['buttons']['default']['Styles']) && version_compare($editor['installed version'], '3.2.1', '>=') && !empty($config['stylesSet'])) {
  522. if ($styles = wysiwyg_ckeditor_settings_parse_styles($config['stylesSet'])) {
  523. $settings['stylesSet'] = $styles;
  524. }
  525. }
  526. $check_if_set = array(
  527. 'forcePasteAsPlainText',
  528. 'language',
  529. 'pasteFromWordNumberedHeadingToList',
  530. 'pasteFromWordPromptCleanup',
  531. 'pasteFromWordRemoveFontStyles',
  532. 'pasteFromWordRemoveStyles',
  533. 'simple_source_formatting',
  534. 'toolbarLocation',
  535. );
  536. foreach ($check_if_set as $setting_name) {
  537. if (isset($config[$setting_name])) {
  538. $settings[$setting_name] = $config[$setting_name];
  539. }
  540. }
  541. if (isset($config['resize_enabled'])) {
  542. // CKEditor performs a type-agnostic comparison on this particular setting.
  543. $settings['resize_enabled'] = (bool) $config['resize_enabled'];
  544. }
  545. $settings['toolbar'] = array();
  546. $supports_groups = version_compare($editor['installed version'], '3.6.0', '>=');
  547. $use_default_groups = $supports_groups && !empty($config['default_toolbar_grouping']);
  548. if (!empty($config['buttons'])) {
  549. $extra_plugins = array();
  550. $plugins = wysiwyg_get_plugins($editor['name']);
  551. foreach ($config['buttons'] as $plugin => $buttons) {
  552. foreach ($buttons as $button => $enabled) {
  553. // Iterate separately over buttons and extensions properties.
  554. foreach (array('buttons', 'extensions') as $type) {
  555. // Skip unavailable plugins.
  556. if (!isset($plugins[$plugin][$type][$button])) {
  557. continue;
  558. }
  559. // Add buttons.
  560. if ($type == 'buttons') {
  561. if ($use_default_groups) {
  562. $settings['toolbar'][_wysiwyg_ckeditor_group($button)][] = $button;
  563. }
  564. else {
  565. // Use one button row for backwards compatibility.
  566. $settings['toolbar'][] = $button;
  567. }
  568. }
  569. // Add external Drupal plugins to the list of extensions.
  570. if ($type == 'buttons' && !empty($plugins[$plugin]['proxy'])) {
  571. $extra_plugins[] = $button;
  572. }
  573. // Add external plugins to the list of extensions.
  574. elseif ($type == 'buttons' && empty($plugins[$plugin]['internal'])) {
  575. $extra_plugins[] = $plugin;
  576. }
  577. // Add internal buttons that also need to be loaded as extension.
  578. elseif ($type == 'buttons' && !empty($plugins[$plugin]['load'])) {
  579. $extra_plugins[] = $plugin;
  580. }
  581. // Add plain extensions.
  582. elseif ($type == 'extensions' && !empty($plugins[$plugin]['load'])) {
  583. $extra_plugins[] = $plugin;
  584. }
  585. // Allow plugins to add or override global configuration settings.
  586. if (!empty($plugins[$plugin]['options'])) {
  587. $settings = array_merge($settings, $plugins[$plugin]['options']);
  588. }
  589. }
  590. }
  591. }
  592. if (!empty($extra_plugins)) {
  593. $settings['extraPlugins'] = implode(',', $extra_plugins);
  594. }
  595. }
  596. if ($use_default_groups) {
  597. // Organize groups to use lables to improves accessibility.
  598. // http://docs.ckeditor.com/#!/guide/dev_toolbar-section-3.
  599. $groups_toolbar = array();
  600. foreach ($settings['toolbar'] as $group => $items) {
  601. $groups_toolbar[] = array(
  602. 'name' => $group,
  603. 'items' => $items,
  604. );
  605. $settings['toolbar'] = $groups_toolbar;
  606. }
  607. }
  608. else {
  609. // For now, all buttons are placed into one row.
  610. $settings['toolbar'] = array($settings['toolbar']);
  611. }
  612. return $settings;
  613. }
  614. /**
  615. * Parses stylesSet settings string into a stylesSet JavaScript settings array.
  616. *
  617. * @param string $css_classes
  618. * A string containing CSS class definitions to add to the Style dropdown
  619. * list, separated by newlines.
  620. *
  621. * @return array|false
  622. * An array containing the parsed stylesSet definition, or FALSE on parse
  623. * error.
  624. *
  625. * @see wysiwyg_ckeditor_settings_form()
  626. * @see wysiwyg_ckeditor_settings_form_validate_css_classes()
  627. *
  628. * @todo This should be a plugin setting, but Wysiwyg does not support
  629. * plugin-specific settings yet.
  630. */
  631. function wysiwyg_ckeditor_settings_parse_styles($css_classes) {
  632. $set = array();
  633. $input = trim($css_classes);
  634. if (empty($input)) {
  635. return $set;
  636. }
  637. // Handle both Unix and Windows line-endings.
  638. foreach (explode("\n", str_replace("\r", '', $input)) as $line) {
  639. $line = trim($line);
  640. // [label]=[element].[class][.[class]][...] pattern expected.
  641. if (!preg_match('@^.+= *[a-zA-Z0-9]+(\.[a-zA-Z0-9_ -]+)*$@', $line)) {
  642. return FALSE;
  643. }
  644. list($label, $selector) = explode('=', $line, 2);
  645. $classes = explode('.', $selector);
  646. $element = array_shift($classes);
  647. $style = array();
  648. $style['name'] = trim($label);
  649. $style['element'] = trim($element);
  650. if (!empty($classes)) {
  651. $style['attributes']['class'] = implode(' ', array_map('trim', $classes));
  652. }
  653. $set[] = $style;
  654. }
  655. return $set;
  656. }
  657. /**
  658. * Build a JS settings array with global metadata for native external plugins.
  659. */
  660. function _wysiwyg_ckeditor_plugin_meta($editor, $plugin) {
  661. $meta = array();
  662. $name = $plugin['name'];
  663. // Register all plugins that need to be loaded.
  664. if (!empty($plugin['load'])) {
  665. // Add path for native external plugins.
  666. if (empty($plugin['internal']) && isset($plugin['path'])) {
  667. $meta['path'] = base_path() . $plugin['path'] . '/';
  668. }
  669. // Force native internal plugins to use the standard path.
  670. else {
  671. $meta['path'] = base_path() . $editor['library path'] . '/plugins/' . $name . '/';
  672. }
  673. // CKEditor defaults to 'plugin.js' on its own when filename is not set.
  674. if (!empty($plugin['filename'])) {
  675. $meta['fileName'] = $plugin['filename'];
  676. }
  677. }
  678. return $meta;
  679. }
  680. /**
  681. * Build a JS settings array for Drupal plugins loaded via the proxy plugin.
  682. */
  683. function _wysiwyg_ckeditor_proxy_plugin_settings($editor, $profile, $plugins) {
  684. $settings = array();
  685. foreach ($plugins as $name => $plugin) {
  686. // Just need a list of all enabled plugins for each instance.
  687. $settings[$name] = TRUE;
  688. }
  689. return $settings;
  690. }
  691. /**
  692. * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin().
  693. */
  694. function _wysiwyg_ckeditor_plugins($editor) {
  695. $plugins = array(
  696. 'default' => array(
  697. 'buttons' => array(
  698. 'Bold' => t('Bold'), 'Italic' => t('Italic'), 'Underline' => t('Underline'),
  699. 'Strike' => t('Strike through'),
  700. 'JustifyLeft' => t('Align left'), 'JustifyCenter' => t('Center'), 'JustifyRight' => t('Align right'), 'JustifyBlock' => t('Justify'),
  701. 'BulletedList' => t('Insert/Remove Bullet list'), 'NumberedList' => t('Insert/Remove Numbered list'),
  702. 'BidiLtr' => t('Left-to-right'), 'BidiRtl' => t('Right-to-left'),
  703. 'Outdent' => t('Outdent'), 'Indent' => t('Indent'),
  704. 'Undo' => t('Undo'), 'Redo' => t('Redo'),
  705. 'Link' => t('Link'), 'Unlink' => t('Unlink'), 'Anchor' => t('Anchor'),
  706. 'Image' => t('Image'),
  707. 'TextColor' => t('Text color'), 'BGColor' => t('Background color'),
  708. 'Superscript' => t('Superscript'), 'Subscript' => t('Subscript'),
  709. 'Blockquote' => t('Block quote'), 'Source' => t('Source code'),
  710. 'HorizontalRule' => t('Horizontal rule'),
  711. 'Cut' => t('Cut'), 'Copy' => t('Copy'), 'Paste' => t('Paste'),
  712. 'PasteText' => t('Paste Text'), 'PasteFromWord' => t('Paste from Word'),
  713. 'ShowBlocks' => t('Show blocks'),
  714. 'RemoveFormat' => t('Remove format'),
  715. 'SpecialChar' => t('Character map'),
  716. 'Format' => t('HTML block format'), 'Font' => t('Font'), 'FontSize' => t('Font size'), 'Styles' => t('Font style'),
  717. 'Table' => t('Table'),
  718. 'SelectAll' => t('Select all'), 'Find' => t('Search'), 'Replace' => t('Replace'),
  719. 'Flash' => t('Flash'), 'Smiley' => t('Smiley'),
  720. 'CreateDiv' => t('Div container'),
  721. 'Iframe' => t('IFrame'),
  722. 'Maximize' => t('Maximize'),
  723. 'SpellChecker' => t('Check spelling'), 'Scayt' => t('Spell check as you type'),
  724. 'About' => t('About'),
  725. 'Templates' => t('Templates'),
  726. 'CopyFormatting' => t('Copy Formatting'),
  727. ),
  728. 'internal' => TRUE,
  729. ),
  730. );
  731. if (version_compare($editor['installed version'], '3.1.0.4885', '<')) {
  732. unset($plugins['default']['buttons']['CreateDiv']);
  733. }
  734. if (version_compare($editor['installed version'], '3.4.0.5808', '<')) {
  735. unset($plugins['default']['buttons']['BidiLtr']);
  736. unset($plugins['default']['buttons']['BidiRtl']);
  737. }
  738. if (version_compare($editor['installed version'], '3.5.0.6260', '<')) {
  739. unset($plugins['default']['buttons']['Iframe']);
  740. }
  741. if (version_compare($editor['installed version'], '4.6.0', '<')) {
  742. unset($plugins['default']['CopyFormatting']);
  743. }
  744. return $plugins;
  745. }
  746. /**
  747. * Define grouping for ckEditor buttons.
  748. */
  749. function _wysiwyg_ckeditor_group($button) {
  750. switch ($button) {
  751. case 'Source':
  752. $group = 'document';
  753. break;
  754. case 'Cut':
  755. case 'Copy':
  756. case 'Paste':
  757. case 'PasteText':
  758. case 'PasteFromWord':
  759. case 'Undo':
  760. case 'Redo':
  761. $group = 'clipboard';
  762. break;
  763. case 'Find':
  764. case 'Replace':
  765. case 'SelectAll':
  766. case 'SpellChecker':
  767. case 'Scayt':
  768. $group = 'editing';
  769. break;
  770. case 'Bold':
  771. case 'Italic':
  772. case 'Underline':
  773. case 'Strike':
  774. case 'Subscript':
  775. case 'Superscript':
  776. $group = 'basicstyles';
  777. break;
  778. case 'RemoveFormat':
  779. case 'CopyFormatting':
  780. $group = 'cleanup';
  781. break;
  782. case 'NumberedList':
  783. case 'BulletedList':
  784. case 'Outdent':
  785. case 'Indent':
  786. case 'Blockquote':
  787. case 'CreateDiv':
  788. case 'JustifyLeft':
  789. case 'JustifyCenter':
  790. case 'JustifyRight':
  791. case 'JustifyBlock':
  792. case 'BidiLtr':
  793. case 'BidiRtl':
  794. $group = 'paragraph';
  795. break;
  796. case 'Link':
  797. case 'Unlink':
  798. case 'Anchor':
  799. $group = 'links';
  800. break;
  801. case 'Image':
  802. case 'Flash':
  803. case 'Table':
  804. case 'HorizontalRule':
  805. case 'Smiley':
  806. case 'SpecialChar':
  807. case 'Iframe':
  808. case 'Templates':
  809. $group = 'insert';
  810. break;
  811. case 'Styles':
  812. case 'Format':
  813. case 'Font':
  814. case 'FontSize':
  815. $group = 'styles';
  816. break;
  817. case 'TextColor':
  818. case 'BGColor':
  819. $group = 'colors';
  820. break;
  821. case 'Maximize':
  822. case 'ShowBlocks':
  823. case 'About':
  824. $group = 'tools';
  825. break;
  826. default:
  827. $group = 'other';
  828. }
  829. return $group;
  830. }
  831. /**
  832. * Determine if string is supposed to be ACF obj format.
  833. *
  834. * @see http://docs.ckeditor.com/#!/guide/dev_allowed_content_rules
  835. */
  836. function _wysiwyg_ckeditor_settings_acf_is_obj($string) {
  837. if (strstr($string, ':') === FALSE) {
  838. return FALSE;
  839. }
  840. return TRUE;
  841. }