| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | Copyright CiviCRM LLC. All rights reserved. | |
| 5 | | | |
| 6 | | This work is published under the GNU AGPLv3 license with some | |
| 7 | | permitted exceptions and without any warranty. For full license | |
| 8 | | and copyright information, see https://civicrm.org/licensing | |
| 9 | +--------------------------------------------------------------------+ |
| 10 | */ |
| 11 | |
| 12 | /** |
| 13 | * |
| 14 | * @package CRM |
| 15 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
| 16 | */ |
| 17 | |
| 18 | use CRM_Ckeditor4_ExtensionUtil as E; |
| 19 | |
| 20 | /** |
| 21 | * Form for configuring CKEditor options. |
| 22 | */ |
| 23 | class CRM_Ckeditor4_Form_CKEditorConfig extends CRM_Core_Form { |
| 24 | |
| 25 | const CONFIG_FILEPATH = '[civicrm.files]/persist/crm-ckeditor-'; |
| 26 | |
| 27 | /** |
| 28 | * @var bool |
| 29 | */ |
| 30 | public $submitOnce = TRUE; |
| 31 | |
| 32 | /** |
| 33 | * Settings that cannot be configured in "advanced options" |
| 34 | * |
| 35 | * @var array |
| 36 | */ |
| 37 | public $blackList = [ |
| 38 | 'on', |
| 39 | 'skin', |
| 40 | 'extraPlugins', |
| 41 | 'toolbarGroups', |
| 42 | 'removeButtons', |
| 43 | 'customConfig', |
| 44 | 'filebrowserBrowseUrl', |
| 45 | 'filebrowserImageBrowseUrl', |
| 46 | 'filebrowserFlashBrowseUrl', |
| 47 | 'filebrowserUploadUrl', |
| 48 | 'filebrowserImageUploadUrl', |
| 49 | 'filebrowserFlashUploadUrl', |
| 50 | ]; |
| 51 | |
| 52 | /** |
| 53 | * Prepare form |
| 54 | */ |
| 55 | public function preProcess() { |
| 56 | CRM_Utils_Request::retrieve('preset', 'String', $this, FALSE, 'default', 'GET'); |
| 57 | |
| 58 | CRM_Utils_System::appendBreadCrumb([ |
| 59 | [ |
| 60 | 'url' => CRM_Utils_System::url('civicrm/admin/setting/preferences/display', 'reset=1'), |
| 61 | 'title' => ts('Display Preferences'), |
| 62 | ], |
| 63 | ]); |
| 64 | |
| 65 | // Initial build |
| 66 | if (empty($_POST['qfKey'])) { |
| 67 | $this->addResources(); |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Add resources during initial build or rebuild |
| 73 | * |
| 74 | * @throws CRM_Core_Exception |
| 75 | */ |
| 76 | public function addResources() { |
| 77 | $settings = $this->getConfigSettings(); |
| 78 | |
| 79 | CRM_Core_Resources::singleton() |
| 80 | ->addScriptFile('civicrm', 'bower_components/ckeditor/ckeditor.js', 0, 'page-header') |
| 81 | ->addScriptFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/js/fulltoolbareditor.js', 1) |
| 82 | ->addScriptFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js', 2) |
| 83 | ->addScriptFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/js/toolbarmodifier.js', 3) |
| 84 | ->addScriptFile('ckeditor4', 'js/admin.ckeditor-configurator.js', 10) |
| 85 | ->addStyleFile('civicrm', 'bower_components/ckeditor/samples/toolbarconfigurator/css/fontello.css') |
| 86 | ->addStyleFile('civicrm', 'bower_components/ckeditor/samples/css/samples.css') |
| 87 | ->addVars('ckConfig', [ |
| 88 | 'plugins' => array_values($this->getCKPlugins()), |
| 89 | 'blacklist' => $this->blackList, |
| 90 | 'settings' => $settings, |
| 91 | ]); |
| 92 | |
| 93 | $configUrl = self::getConfigUrl($this->get('preset')) ?: self::getConfigUrl('default'); |
| 94 | |
| 95 | $this->assign('preset', $this->get('preset')); |
| 96 | $this->assign('presets', CRM_Core_OptionGroup::values('wysiwyg_presets', FALSE, FALSE, FALSE, NULL, 'label', TRUE, FALSE, 'name')); |
| 97 | $this->assign('skins', $this->getCKSkins()); |
| 98 | $this->assign('skin', CRM_Utils_Array::value('skin', $settings)); |
| 99 | $this->assign('extraPlugins', CRM_Utils_Array::value('extraPlugins', $settings)); |
| 100 | $this->assign('configUrl', $configUrl); |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Build form |
| 105 | */ |
| 106 | public function buildQuickForm() { |
| 107 | $revertConfirm = json_encode(ts('Are you sure you want to revert all changes?')); |
| 108 | $this->addButtons([ |
| 109 | [ |
| 110 | 'type' => 'next', |
| 111 | 'name' => ts('Save'), |
| 112 | ], |
| 113 | // Hidden button used to refresh form |
| 114 | [ |
| 115 | 'type' => 'submit', |
| 116 | 'class' => 'hiddenElement', |
| 117 | 'name' => ts('Save'), |
| 118 | ], |
| 119 | [ |
| 120 | 'type' => 'cancel', |
| 121 | 'name' => ts('Cancel'), |
| 122 | ], |
| 123 | [ |
| 124 | 'type' => 'refresh', |
| 125 | 'name' => ts('Revert to Default'), |
| 126 | 'icon' => 'fa-undo', |
| 127 | 'js' => ['onclick' => "return confirm($revertConfirm);"], |
| 128 | ], |
| 129 | ]); |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Handle form submission |
| 134 | */ |
| 135 | public function postProcess() { |
| 136 | if (!empty($_POST[$this->getButtonName('refresh')])) { |
| 137 | self::deleteConfigFile($this->get('preset')); |
| 138 | self::setConfigDefault(); |
| 139 | } |
| 140 | else { |
| 141 | if (!empty($_POST[$this->getButtonName('next')])) { |
| 142 | $this->save($_POST); |
| 143 | CRM_Core_Session::setStatus(ts("You may need to clear your browser's cache to see the changes in CiviCRM."), ts('CKEditor Saved'), 'success'); |
| 144 | } |
| 145 | // The "submit" hidden button saves but does not redirect |
| 146 | if (!empty($_POST[$this->getButtonName('submit')])) { |
| 147 | $this->save($_POST); |
| 148 | $this->addResources(); |
| 149 | } |
| 150 | else { |
| 151 | CRM_Core_Session::singleton()->pushUserContext(CRM_Utils_System::url('civicrm/admin/ckeditor', ['reset' => 1])); |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Generate the config js file based on posted data. |
| 158 | * |
| 159 | * @param array $params |
| 160 | */ |
| 161 | public function save($params) { |
| 162 | $config = self::fileHeader() |
| 163 | // Standardize line-endings |
| 164 | . preg_replace('~\R~u', "\n", $params['config']); |
| 165 | |
| 166 | // Generate a whitelist of allowed config params |
| 167 | $allOptions = json_decode(file_get_contents(E::path('js/ck-options.json')), TRUE); |
| 168 | // These two aren't really blacklisted they're just in a different part of the form |
| 169 | $blackList = array_diff($this->blackList, ['skin', 'extraPlugins']); |
| 170 | // All options minus blacklist = whitelist |
| 171 | $whiteList = array_diff(array_column($allOptions, 'id'), $blackList); |
| 172 | |
| 173 | // Save whitelisted params starting with config_ |
| 174 | foreach ($params as $key => $val) { |
| 175 | $val = trim($val); |
| 176 | if (strpos($key, 'config_') === 0 && strlen($val) && in_array(substr($key, 7), $whiteList)) { |
| 177 | if ($val != 'true' && $val != 'false' && $val != 'null' && $val[0] != '{' && $val[0] != '[' && !is_numeric($val)) { |
| 178 | $val = '"' . $val . '"'; |
| 179 | } |
| 180 | try { |
| 181 | $val = CRM_Utils_JS::encode(CRM_Utils_JS::decode($val, TRUE)); |
| 182 | $pos = strrpos($config, '};'); |
| 183 | $key = preg_replace('/^config_/', 'config.', $key); |
| 184 | $setting = "\n\t{$key} = {$val};\n"; |
| 185 | $config = substr_replace($config, $setting, $pos, 0); |
| 186 | } |
| 187 | catch (CRM_Core_Exception $e) { |
| 188 | CRM_Core_Session::setStatus(ts("Error saving %1.", [1 => $key]), ts('Invalid Value'), 'error'); |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | self::saveConfigFile($this->get('preset'), $config); |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * Get available CKEditor plugin list. |
| 197 | * |
| 198 | * @return array |
| 199 | */ |
| 200 | private function getCKPlugins() { |
| 201 | $plugins = []; |
| 202 | $pluginDir = Civi::paths()->getPath('[civicrm.root]/bower_components/ckeditor/plugins'); |
| 203 | |
| 204 | foreach (glob($pluginDir . '/*', GLOB_ONLYDIR) as $dir) { |
| 205 | $dir = rtrim(str_replace('\\', '/', $dir), '/'); |
| 206 | $name = substr($dir, strrpos($dir, '/') + 1); |
| 207 | $dir = CRM_Utils_File::addTrailingSlash($dir, '/'); |
| 208 | if (is_file($dir . 'plugin.js')) { |
| 209 | $plugins[$name] = [ |
| 210 | 'id' => $name, |
| 211 | 'text' => ucfirst($name), |
| 212 | 'icon' => NULL, |
| 213 | ]; |
| 214 | if (is_dir($dir . "icons")) { |
| 215 | if (is_file($dir . "icons/$name.png")) { |
| 216 | $plugins[$name]['icon'] = "bower_components/ckeditor/plugins/$name/icons/$name.png"; |
| 217 | } |
| 218 | elseif (glob($dir . "icons/*.png")) { |
| 219 | $icon = CRM_Utils_Array::first(glob($dir . "icons/*.png")); |
| 220 | $icon = rtrim(str_replace('\\', '/', $icon), '/'); |
| 221 | $plugins[$name]['icon'] = "bower_components/ckeditor/plugins/$name/icons/" . substr($icon, strrpos($icon, '/') + 1); |
| 222 | } |
| 223 | } |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | return $plugins; |
| 228 | } |
| 229 | |
| 230 | /** |
| 231 | * Get available CKEditor skins. |
| 232 | * |
| 233 | * @return array |
| 234 | */ |
| 235 | private function getCKSkins() { |
| 236 | $skins = []; |
| 237 | $skinDir = Civi::paths()->getPath('[civicrm.root]/bower_components/ckeditor/skins'); |
| 238 | foreach (glob($skinDir . '/*', GLOB_ONLYDIR) as $dir) { |
| 239 | $dir = rtrim(str_replace('\\', '/', $dir), '/'); |
| 240 | $skins[] = substr($dir, strrpos($dir, '/') + 1); |
| 241 | } |
| 242 | return $skins; |
| 243 | } |
| 244 | |
| 245 | /** |
| 246 | * @return array |
| 247 | */ |
| 248 | private function getConfigSettings() { |
| 249 | $matches = $result = []; |
| 250 | $file = self::getConfigFile($this->get('preset')) ?: self::getConfigFile('default'); |
| 251 | $result['skin'] = 'moono'; |
| 252 | if ($file) { |
| 253 | $contents = file_get_contents($file); |
| 254 | preg_match_all("/\sconfig\.(\w+)\s?=\s?([^;]*);/", $contents, $matches); |
| 255 | foreach ($matches[1] as $i => $match) { |
| 256 | $result[$match] = trim($matches[2][$i], ' "\''); |
| 257 | } |
| 258 | } |
| 259 | return $result; |
| 260 | } |
| 261 | |
| 262 | /** |
| 263 | * @param string $preset |
| 264 | * Omit to get an array of all presets |
| 265 | * @return array|null|string |
| 266 | */ |
| 267 | public static function getConfigUrl($preset = NULL) { |
| 268 | $items = []; |
| 269 | $presets = CRM_Core_OptionGroup::values('wysiwyg_presets', FALSE, FALSE, FALSE, NULL, 'name'); |
| 270 | foreach ($presets as $key => $name) { |
| 271 | if (self::getConfigFile($name)) { |
| 272 | $items[$name] = Civi::paths()->getUrl(self::CONFIG_FILEPATH . $name . '.js', 'absolute'); |
| 273 | } |
| 274 | } |
| 275 | return $preset ? CRM_Utils_Array::value($preset, $items) : $items; |
| 276 | } |
| 277 | |
| 278 | /** |
| 279 | * @param string $preset |
| 280 | * |
| 281 | * @return null|string |
| 282 | */ |
| 283 | public static function getConfigFile($preset = 'default') { |
| 284 | $fileName = Civi::paths()->getPath(self::CONFIG_FILEPATH . $preset . '.js'); |
| 285 | return is_file($fileName) ? $fileName : NULL; |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * @param string $preset |
| 290 | * @param string $contents |
| 291 | */ |
| 292 | public static function saveConfigFile($preset, $contents) { |
| 293 | $file = Civi::paths()->getPath(self::CONFIG_FILEPATH . $preset . '.js'); |
| 294 | file_put_contents($file, $contents); |
| 295 | } |
| 296 | |
| 297 | /** |
| 298 | * Delete config file. |
| 299 | */ |
| 300 | public static function deleteConfigFile($preset) { |
| 301 | $file = self::getConfigFile($preset); |
| 302 | if ($file) { |
| 303 | unlink($file); |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | /** |
| 308 | * Create default config file if it doesn't exist |
| 309 | */ |
| 310 | public static function setConfigDefault() { |
| 311 | if (!self::getConfigFile()) { |
| 312 | $config = self::fileHeader() . "CKEDITOR.editorConfig = function( config ) {\n\tconfig.allowedContent = true;\n};\n"; |
| 313 | // Make sure directories exist |
| 314 | if (!is_dir(Civi::paths()->getPath('[civicrm.files]/persist'))) { |
| 315 | mkdir(Civi::paths()->getPath('[civicrm.files]/persist')); |
| 316 | } |
| 317 | $newFileName = Civi::paths()->getPath(self::CONFIG_FILEPATH . 'default.js'); |
| 318 | file_put_contents($newFileName, $config); |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | /** |
| 323 | * @return string |
| 324 | */ |
| 325 | public static function fileHeader() { |
| 326 | return "/**\n" |
| 327 | . " * CKEditor config file auto-generated by CiviCRM (" . date('Y-m-d H:i:s') . ").\n" |
| 328 | . " *\n" |
| 329 | . " * Note: This file will be overwritten if settings are modified at:\n" |
| 330 | . " * @link " . CRM_Utils_System::url('civicrm/admin/ckeditor', NULL, TRUE, NULL, FALSE) . "\n" |
| 331 | . " */\n"; |
| 332 | } |
| 333 | |
| 334 | } |