Merge pull request #11752 from colemanw/CKEditor
[civicrm-core.git] / CRM / Admin / Page / CKEditorConfig.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2018 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2018
32 */
33
34 /**
35 * Page for configuring CKEditor options.
36 *
37 * Note that while this is implemented as a CRM_Core_Page, it is actually a form.
38 * Because the form needs to be submitted and refreshed via javascript, it seemed like
39 * Quickform and CRM_Core_Form/Controller might get in the way.
40 */
41 class CRM_Admin_Page_CKEditorConfig extends CRM_Core_Page {
42
43 const CONFIG_FILEPATH = '[civicrm.files]/persist/crm-ckeditor-';
44
45 /**
46 * Settings that cannot be configured in "advanced options"
47 *
48 * @var array
49 */
50 public $blackList = array(
51 'on',
52 'skin',
53 'extraPlugins',
54 'toolbarGroups',
55 'removeButtons',
56 'filebrowserBrowseUrl',
57 'filebrowserImageBrowseUrl',
58 'filebrowserFlashBrowseUrl',
59 'filebrowserUploadUrl',
60 'filebrowserImageUploadUrl',
61 'filebrowserFlashUploadUrl',
62 );
63
64 public $preset;
65
66 /**
67 * Run page.
68 *
69 * @return string
70 */
71 public function run() {
72 $this->preset = CRM_Utils_Array::value('preset', $_REQUEST, 'default');
73
74 // If the form was submitted, take appropriate action.
75 if (!empty($_POST['revert'])) {
76 self::deleteConfigFile($this->preset);
77 self::setConfigDefault();
78 }
79 elseif (!empty($_POST['config'])) {
80 $this->save($_POST);
81 }
82
83 $settings = $this->getConfigSettings();
84
85 CRM_Core_Resources::singleton()
86 ->addScriptFile('civicrm', 'bower_components/ckeditor/ckeditor.js', 0, 'page-header')
87 ->addScriptFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/js/fulltoolbareditor.js', 1)
88 ->addScriptFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js', 2)
89 ->addScriptFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/js/toolbarmodifier.js', 3)
90 ->addScriptFile('civicrm', 'js/wysiwyg/admin.ckeditor-configurator.js', 10)
91 ->addStyleFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/css/fontello.css')
92 ->addStyleFile('civicrm', 'bower_components/ckeditor/samples/css/samples.css')
93 ->addVars('ckConfig', array(
94 'plugins' => array_values($this->getCKPlugins()),
95 'blacklist' => $this->blackList,
96 'settings' => $settings,
97 ));
98
99 $configUrl = self::getConfigUrl($this->preset);
100 if (!$configUrl) {
101 $configUrl = self::getConfigUrl('default');
102 }
103
104 $this->assign('preset', $this->preset);
105 $this->assign('presets', CRM_Core_OptionGroup::values('wysiwyg_presets', FALSE, FALSE, FALSE, NULL, 'label', TRUE, FALSE, 'name'));
106 $this->assign('skins', $this->getCKSkins());
107 $this->assign('skin', CRM_Utils_Array::value('skin', $settings));
108 $this->assign('extraPlugins', CRM_Utils_Array::value('extraPlugins', $settings));
109 $this->assign('configUrl', $configUrl);
110 $this->assign('revertConfirm', htmlspecialchars(ts('Are you sure you want to revert all changes?', array('escape' => 'js'))));
111
112 CRM_Utils_System::appendBreadCrumb(array(array(
113 'url' => CRM_Utils_System::url('civicrm/admin/setting/preferences/display', 'reset=1'),
114 'title' => ts('Display Preferences'),
115 )));
116
117 return parent::run();
118 }
119
120 /**
121 * Generate the config js file based on posted data.
122 *
123 * @param array $params
124 */
125 public function save($params) {
126 $config = self::fileHeader()
127 // Standardize line-endings
128 . preg_replace('~\R~u', "\n", $params['config']);
129
130 // Use all params starting with config_
131 foreach ($params as $key => $val) {
132 $val = trim($val);
133 if (strpos($key, 'config_') === 0 && strlen($val)) {
134 if ($val != 'true' && $val != 'false' && $val != 'null' && $val[0] != '{' && $val[0] != '[' && !is_numeric($val)) {
135 $val = json_encode($val, JSON_UNESCAPED_SLASHES);
136 }
137 elseif ($val[0] == '{' || $val[0] == '[') {
138 if (!is_array(json_decode($val, TRUE))) {
139 // Invalid JSON. Do not save.
140 continue;
141 }
142 }
143 $pos = strrpos($config, '};');
144 $key = preg_replace('/^config_/', 'config.', $key);
145 $setting = "\n\t{$key} = {$val};\n";
146 $config = substr_replace($config, $setting, $pos, 0);
147 }
148 }
149 self::saveConfigFile($this->preset, $config);
150 if (!empty($params['save'])) {
151 CRM_Core_Session::setStatus(ts("You may need to clear your browser's cache to see the changes in CiviCRM."), ts('CKEditor Saved'), 'success');
152 }
153 }
154
155 /**
156 * Get available CKEditor plugin list.
157 *
158 * @return array
159 */
160 private function getCKPlugins() {
161 $plugins = array();
162 $pluginDir = Civi::paths()->getPath('[civicrm.root]/bower_components/ckeditor/plugins');
163
164 foreach (glob($pluginDir . '/*', GLOB_ONLYDIR) as $dir) {
165 $dir = rtrim(str_replace('\\', '/', $dir), '/');
166 $name = substr($dir, strrpos($dir, '/') + 1);
167 $dir = CRM_Utils_file::addTrailingSlash($dir, '/');
168 if (is_file($dir . 'plugin.js')) {
169 $plugins[$name] = array(
170 'id' => $name,
171 'text' => ucfirst($name),
172 'icon' => NULL,
173 );
174 if (is_dir($dir . "icons")) {
175 if (is_file($dir . "icons/$name.png")) {
176 $plugins[$name]['icon'] = "bower_components/ckeditor/plugins/$name/icons/$name.png";
177 }
178 elseif (glob($dir . "icons/*.png")) {
179 $icon = CRM_Utils_Array::first(glob($dir . "icons/*.png"));
180 $icon = rtrim(str_replace('\\', '/', $icon), '/');
181 $plugins[$name]['icon'] = "bower_components/ckeditor/plugins/$name/icons/" . substr($icon, strrpos($icon, '/') + 1);
182 }
183 }
184 }
185 }
186
187 return $plugins;
188 }
189
190 /**
191 * Get available CKEditor skins.
192 *
193 * @return array
194 */
195 private function getCKSkins() {
196 $skins = array();
197 $skinDir = Civi::paths()->getPath('[civicrm.root]/bower_components/ckeditor/skins');
198 foreach (glob($skinDir . '/*', GLOB_ONLYDIR) as $dir) {
199 $dir = rtrim(str_replace('\\', '/', $dir), '/');
200 $skins[] = substr($dir, strrpos($dir, '/') + 1);
201 }
202 return $skins;
203 }
204
205 /**
206 * @return array
207 */
208 private function getConfigSettings() {
209 $matches = $result = array();
210 $file = self::getConfigFile($this->preset);
211 if (!$file) {
212 $file = self::getConfigFile('default');
213 }
214 $result['skin'] = 'moono';
215 if ($file) {
216 $contents = file_get_contents($file);
217 preg_match_all("/\sconfig\.(\w+)\s?=\s?([^;]*);/", $contents, $matches);
218 foreach ($matches[1] as $i => $match) {
219 $result[$match] = trim($matches[2][$i], ' "\'');
220 }
221 }
222 return $result;
223 }
224
225 /**
226 * @param string $preset
227 * Omit to get an array of all presets
228 * @return array|null|string
229 */
230 public static function getConfigUrl($preset = NULL) {
231 $items = array();
232 $presets = CRM_Core_OptionGroup::values('wysiwyg_presets', FALSE, FALSE, FALSE, NULL, 'name');
233 foreach ($presets as $key => $name) {
234 if (self::getConfigFile($name)) {
235 $items[$name] = Civi::paths()->getUrl(self::CONFIG_FILEPATH . $name . '.js', 'absolute');
236 }
237 }
238 return $preset ? CRM_Utils_Array::value($preset, $items) : $items;
239 }
240
241 /**
242 * @param string $preset
243 *
244 * @return null|string
245 */
246 public static function getConfigFile($preset = 'default') {
247 $fileName = Civi::paths()->getPath(self::CONFIG_FILEPATH . $preset . '.js');
248 return is_file($fileName) ? $fileName : NULL;
249 }
250
251 /**
252 * @param string $contents
253 */
254 public static function saveConfigFile($preset, $contents) {
255 $file = Civi::paths()->getPath(self::CONFIG_FILEPATH . $preset . '.js');
256 file_put_contents($file, $contents);
257 }
258
259 /**
260 * Delete config file.
261 */
262 public static function deleteConfigFile($preset) {
263 $file = self::getConfigFile($preset);
264 if ($file) {
265 unlink($file);
266 }
267 }
268
269 /**
270 * Create default config file if it doesn't exist
271 */
272 public static function setConfigDefault() {
273 if (!self::getConfigFile()) {
274 $config = self::fileHeader() . "CKEDITOR.editorConfig = function( config ) {\n\tconfig.allowedContent = true;\n};\n";
275 // Make sure directories exist
276 if (!is_dir(Civi::paths()->getPath('[civicrm.files]/persist'))) {
277 mkdir(Civi::paths()->getPath('[civicrm.files]/persist'));
278 }
279 $newFileName = Civi::paths()->getPath('[civicrm.files]/persist/crm-ckeditor-default.js');
280 file_put_contents($newFileName, $config);
281 }
282 }
283
284 /**
285 * @return string
286 */
287 public static function fileHeader() {
288 return "/**\n"
289 . " * CKEditor config file auto-generated by CiviCRM (" . date('Y-m-d H:i:s') . ").\n"
290 . " *\n"
291 . " * Note: This file will be overwritten if settings are modified at:\n"
292 . " * @link " . CRM_Utils_System::url('civicrm/admin/ckeditor', NULL, TRUE, NULL, FALSE) . "\n"
293 . " */\n";
294 }
295
296 }