001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.widgets; 029 030import org.opencms.ade.configuration.CmsADEConfigData; 031import org.opencms.ade.galleries.shared.I_CmsGalleryProviderConstants; 032import org.opencms.file.CmsFile; 033import org.opencms.file.CmsObject; 034import org.opencms.file.CmsRequestContext; 035import org.opencms.file.CmsResource; 036import org.opencms.file.CmsResourceFilter; 037import org.opencms.i18n.CmsEncoder; 038import org.opencms.i18n.CmsLocaleManager; 039import org.opencms.i18n.CmsMessages; 040import org.opencms.json.JSONException; 041import org.opencms.json.JSONObject; 042import org.opencms.main.CmsException; 043import org.opencms.main.CmsLog; 044import org.opencms.main.OpenCms; 045import org.opencms.main.OpenCmsSpellcheckHandler; 046import org.opencms.util.CmsJsonUtil; 047import org.opencms.util.CmsMacroResolver; 048import org.opencms.util.CmsStringUtil; 049import org.opencms.workplace.CmsWorkplace; 050import org.opencms.workplace.editors.CmsEditorDisplayOptions; 051import org.opencms.workplace.editors.I_CmsEditorCssHandler; 052import org.opencms.xml.content.I_CmsXmlContentHandler.DisplayType; 053import org.opencms.xml.types.A_CmsXmlContentValue; 054 055import java.io.UnsupportedEncodingException; 056import java.util.Arrays; 057import java.util.Collections; 058import java.util.HashSet; 059import java.util.Iterator; 060import java.util.List; 061import java.util.Locale; 062import java.util.Map; 063import java.util.Properties; 064import java.util.Set; 065 066import org.apache.commons.logging.Log; 067 068import com.google.common.collect.Lists; 069import com.google.common.collect.Sets; 070 071/** 072 * Provides a widget that creates a rich input field using the matching component, for use on a widget dialog.<p> 073 * 074 * The matching component is determined by checking the installed editors for the best matching component to use.<p> 075 * 076 * @since 6.0.1 077 */ 078public class CmsHtmlWidget extends A_CmsHtmlWidget implements I_CmsADEWidget { 079 080 /** Sitemap attribute key for configuring the TinyMCE JSON configuration. */ 081 public static final String ATTR_TEMPLATE_EDITOR_CONFIGFILE = "template.editor.configfile"; 082 083 /** Labels for the default block format options. */ 084 public static final Map<String, String> TINYMCE_DEFAULT_BLOCK_FORMAT_LABELS = Collections.unmodifiableMap( 085 CmsStringUtil.splitAsMap( 086 "p:Paragraph|address:Address|pre:Pre|h1:Header 1|h2:Header 2|h3:Header 3|h4:Header 4|h5:Header 5|h6:Header 6", 087 "|", 088 ":")); 089 090 /** The log object for this class. */ 091 private static final Log LOG = CmsLog.getLog(CmsHtmlWidget.class); 092 093 /** The editor widget to use depending on the current users settings, current browser and installed editors. */ 094 private I_CmsWidget m_editorWidget; 095 096 /** 097 * Creates a new html editing widget.<p> 098 */ 099 public CmsHtmlWidget() { 100 101 // empty constructor is required for class registration 102 this(""); 103 } 104 105 /** 106 * Creates a new html editing widget with the given configuration.<p> 107 * 108 * @param configuration the configuration to use 109 */ 110 public CmsHtmlWidget(String configuration) { 111 112 super(configuration); 113 } 114 115 /** 116 * Returns the WYSIWYG editor configuration as a JSON object.<p> 117 * 118 * @param widgetOptions the options for the wysiwyg widget 119 * @param cms the OpenCms context 120 * @param resource the edited resource 121 * @param contentLocale the edited content locale 122 * 123 * @return the configuration 124 */ 125 public static JSONObject getJSONConfiguration( 126 CmsHtmlWidgetOption widgetOptions, 127 CmsObject cms, 128 CmsResource resource, 129 Locale contentLocale) { 130 131 JSONObject result = new JSONObject(); 132 133 CmsEditorDisplayOptions options = OpenCms.getWorkplaceManager().getEditorDisplayOptions(); 134 Properties displayOptions = options.getDisplayOptions(cms); 135 try { 136 if (options.showElement("gallery.enhancedoptions", displayOptions)) { 137 result.put("cmsGalleryEnhancedOptions", true); 138 } 139 if (options.showElement("gallery.usethickbox", displayOptions)) { 140 result.put("cmsGalleryUseThickbox", true); 141 } 142 if (widgetOptions.isAllowScripts()) { 143 result.put("allowscripts", Boolean.TRUE); 144 } 145 result.put("fullpage", widgetOptions.isFullPage()); 146 List<String> toolbarItems = widgetOptions.getButtonBarShownItems(); 147 result.put("toolbar_items", toolbarItems); 148 Locale workplaceLocale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms); 149 String editorHeight = widgetOptions.getEditorHeight(); 150 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(editorHeight)) { 151 editorHeight = editorHeight.replaceAll("px", ""); 152 result.put("height", editorHeight); 153 } 154 // set CSS style sheet for current editor widget if configured 155 boolean cssConfigured = false; 156 String cssPath = ""; 157 if (widgetOptions.useCss()) { 158 cssPath = widgetOptions.getCssPath(); 159 // set the CSS path to null (the created configuration String passed to JS will not include this path then) 160 widgetOptions.setCssPath(null); 161 cssConfigured = true; 162 } else if (OpenCms.getWorkplaceManager().getEditorCssHandlers().size() > 0) { 163 Iterator<I_CmsEditorCssHandler> i = OpenCms.getWorkplaceManager().getEditorCssHandlers().iterator(); 164 try { 165 String editedResourceSitePath = resource == null ? null : cms.getSitePath(resource); 166 while (i.hasNext()) { 167 I_CmsEditorCssHandler handler = i.next(); 168 if (handler.matches(cms, editedResourceSitePath)) { 169 cssPath = handler.getUriStyleSheet(cms, editedResourceSitePath); 170 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(cssPath)) { 171 cssConfigured = true; 172 } 173 break; 174 } 175 } 176 } catch (Exception e) { 177 // ignore, CSS could not be set 178 } 179 } 180 181 String contentCss = CmsWorkplace.getStaticResourceUri("/editors/tinymce/opencms_content_default.css"); 182 if (cssConfigured) { 183 contentCss += "," + OpenCms.getLinkManager().substituteLink(cms, cssPath); 184 } 185 result.put("content_css", contentCss); 186 187 if (widgetOptions.showStylesFormat()) { 188 try { 189 CmsFile file = cms.readFile(widgetOptions.getStylesFormatPath()); 190 String characterEncoding = OpenCms.getSystemInfo().getDefaultEncoding(); 191 result.put("style_formats", new String(file.getContents(), characterEncoding)); 192 } catch (CmsException cmsException) { 193 LOG.error("Can not open file:" + widgetOptions.getStylesFormatPath(), cmsException); 194 } catch (UnsupportedEncodingException ex) { 195 LOG.error(ex); 196 } 197 } 198 if (widgetOptions.isImportCss()) { 199 result.put("importCss", true); 200 } 201 String formatSelectOptions = widgetOptions.getFormatSelectOptions(); 202 if (!CmsStringUtil.isEmpty(formatSelectOptions) 203 && !widgetOptions.isButtonHidden(CmsHtmlWidgetOption.OPTION_FORMATSELECT)) { 204 result.put("block_formats", getTinyMceBlockFormats(formatSelectOptions)); 205 } 206 Boolean pasteText = Boolean.valueOf( 207 OpenCms.getWorkplaceManager().getWorkplaceEditorManager().getEditorParameter( 208 cms, 209 "tinymce", 210 "paste_text")); 211 JSONObject pasteOptions = new JSONObject(); 212 pasteOptions.put("paste_text_sticky_default", pasteText); 213 pasteOptions.put("paste_text_sticky", pasteText); 214 result.put("pasteOptions", pasteOptions); 215 // if spell checking is enabled, add the spell handler URL 216 if (OpenCmsSpellcheckHandler.isSpellcheckingEnabled()) { 217 result.put( 218 "spellcheck_url", 219 OpenCms.getLinkManager().substituteLinkForUnknownTarget( 220 cms, 221 OpenCmsSpellcheckHandler.getSpellcheckHandlerPath())); 222 223 result.put("spellcheck_language", contentLocale.getLanguage()); 224 } 225 String typografLocale = CmsTextareaWidget.getTypografLocale(contentLocale); 226 result.put("typograf_locale", typografLocale); 227 String linkDefaultProtocol = widgetOptions.getLinkDefaultProtocol(); 228 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(linkDefaultProtocol)) { 229 result.put("link_default_protocol", linkDefaultProtocol); 230 } 231 232 String editorOptions = widgetOptions.getEditorConfigPath(); 233 editorOptions = getEditorConfigPath(cms, resource, widgetOptions); 234 if (editorOptions != null) { 235 try { 236 CmsResource editorOptionsRes = cms.readResource(editorOptions, CmsResourceFilter.IGNORE_EXPIRATION); 237 CmsFile editorOptionsFile = cms.readFile(editorOptionsRes); 238 String encoding = CmsLocaleManager.getResourceEncoding(cms, editorOptionsRes); 239 String contentAsString = new String(editorOptionsFile.getContents(), encoding); 240 JSONObject directOptions = new JSONObject(contentAsString); 241 // JSON may contain user-readable strings, which we may want to localize, 242 // but we also don't want to accidentally produce invalid JSON, so we recursively 243 // replace macros in string values occuring in the JSON 244 CmsMacroResolver resolver = new CmsMacroResolver(); 245 resolver.setCmsObject(cms); 246 resolver.setMessages(OpenCms.getWorkplaceManager().getMessages(workplaceLocale)); 247 JSONObject replacedOptions = CmsJsonUtil.mapJsonObject(directOptions, val -> { 248 if (val instanceof String) { 249 return resolver.resolveMacros((String)val); 250 } else { 251 return val; 252 } 253 }); 254 result.put("directOptions", replacedOptions); 255 } catch (Exception e) { 256 LOG.error( 257 "Error processing editor options from " + editorOptions + ": " + e.getLocalizedMessage(), 258 e); 259 } 260 } 261 262 } catch (JSONException e) { 263 LOG.error(e.getLocalizedMessage(), e); 264 } 265 return result; 266 } 267 268 /** 269 * Gets the block format configuration string for TinyMCE from the configured format select options.<p> 270 * 271 * @param formatSelectOptions the format select options 272 * 273 * @return the block_formats configuration 274 */ 275 public static String getTinyMceBlockFormats(String formatSelectOptions) { 276 277 String[] options = formatSelectOptions.split(";"); 278 List<String> resultParts = Lists.newArrayList(); 279 for (String option : options) { 280 String label = TINYMCE_DEFAULT_BLOCK_FORMAT_LABELS.get(option); 281 if (label == null) { 282 label = option; 283 } 284 resultParts.add(label + "=" + option); 285 } 286 String result = CmsStringUtil.listAsString(resultParts, ";"); 287 return result; 288 } 289 290 /** 291 * Determines the TinyMCE configuration JSON path for the given widget configuration and edited resource. 292 * 293 * @param cms the CMS context 294 * @param resource the edited resource 295 * @param widgetOptions the widget configuration 296 * @return 297 */ 298 private static String getEditorConfigPath(CmsObject cms, CmsResource resource, CmsHtmlWidgetOption widgetOptions) { 299 300 if (!CmsStringUtil.isEmptyOrWhitespaceOnly(widgetOptions.getEditorConfigPath())) { 301 return widgetOptions.getEditorConfigPath(); 302 } 303 String adeContextPath = (String)cms.getRequestContext().getAttribute( 304 CmsRequestContext.ATTRIBUTE_ADE_CONTEXT_PATH); 305 String pathToCheck = null; 306 if (adeContextPath != null) { 307 pathToCheck = adeContextPath; 308 } else if (resource != null) { 309 pathToCheck = resource.getRootPath(); 310 } else { 311 return null; 312 } 313 CmsADEConfigData config = OpenCms.getADEManager().lookupConfigurationWithCache(cms, pathToCheck); 314 String valueFromSitemapConfig = config.getAttribute(ATTR_TEMPLATE_EDITOR_CONFIGFILE, null); 315 if (valueFromSitemapConfig != null) { 316 valueFromSitemapConfig = valueFromSitemapConfig.trim(); 317 if ("none".equals(valueFromSitemapConfig)) { 318 return null; 319 } 320 return valueFromSitemapConfig; 321 } else { 322 return null; 323 } 324 } 325 326 /** 327 * @see org.opencms.widgets.I_CmsADEWidget#getConfiguration(org.opencms.file.CmsObject, org.opencms.xml.types.A_CmsXmlContentValue, org.opencms.i18n.CmsMessages, org.opencms.file.CmsResource, java.util.Locale) 328 */ 329 public String getConfiguration( 330 CmsObject cms, 331 A_CmsXmlContentValue schemaType, 332 CmsMessages messages, 333 CmsResource resource, 334 Locale contentLocale) { 335 336 JSONObject result = getJSONConfiguration(cms, resource, contentLocale); 337 try { 338 addEmbeddedGalleryOptions(result, cms, schemaType, messages, resource, contentLocale); 339 } catch (JSONException e) { 340 LOG.error(e.getLocalizedMessage(), e); 341 } 342 return result.toString(); 343 } 344 345 /** 346 * @see org.opencms.widgets.I_CmsADEWidget#getCssResourceLinks(org.opencms.file.CmsObject) 347 */ 348 public List<String> getCssResourceLinks(CmsObject cms) { 349 350 // not needed for internal widget 351 return null; 352 } 353 354 /** 355 * @see org.opencms.widgets.I_CmsADEWidget#getDefaultDisplayType() 356 */ 357 public DisplayType getDefaultDisplayType() { 358 359 return DisplayType.wide; 360 } 361 362 /** 363 * @see org.opencms.widgets.I_CmsWidget#getDialogIncludes(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog) 364 */ 365 @Override 366 public String getDialogIncludes(CmsObject cms, I_CmsWidgetDialog widgetDialog) { 367 368 return getEditorWidget(cms, widgetDialog).getDialogIncludes(cms, widgetDialog); 369 } 370 371 /** 372 * @see org.opencms.widgets.I_CmsWidget#getDialogInitCall(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog) 373 */ 374 @Override 375 public String getDialogInitCall(CmsObject cms, I_CmsWidgetDialog widgetDialog) { 376 377 return getEditorWidget(cms, widgetDialog).getDialogInitCall(cms, widgetDialog); 378 } 379 380 /** 381 * @see org.opencms.widgets.I_CmsWidget#getDialogInitMethod(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog) 382 */ 383 @Override 384 public String getDialogInitMethod(CmsObject cms, I_CmsWidgetDialog widgetDialog) { 385 386 return getEditorWidget(cms, widgetDialog).getDialogInitMethod(cms, widgetDialog); 387 } 388 389 /** 390 * @see org.opencms.widgets.I_CmsWidget#getDialogWidget(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog, org.opencms.widgets.I_CmsWidgetParameter) 391 */ 392 public String getDialogWidget(CmsObject cms, I_CmsWidgetDialog widgetDialog, I_CmsWidgetParameter param) { 393 394 return getEditorWidget(cms, widgetDialog).getDialogWidget(cms, widgetDialog, param); 395 } 396 397 /** 398 * @see org.opencms.widgets.I_CmsADEWidget#getInitCall() 399 */ 400 public String getInitCall() { 401 402 // not needed for internal widget 403 return null; 404 } 405 406 /** 407 * @see org.opencms.widgets.I_CmsADEWidget#getJavaScriptResourceLinks(org.opencms.file.CmsObject) 408 */ 409 public List<String> getJavaScriptResourceLinks(CmsObject cms) { 410 411 // not needed for internal widget 412 return null; 413 } 414 415 /** 416 * @see org.opencms.widgets.I_CmsADEWidget#getWidgetName() 417 */ 418 public String getWidgetName() { 419 420 return CmsHtmlWidget.class.getName(); 421 } 422 423 /** 424 * @see org.opencms.widgets.I_CmsADEWidget#isInternal() 425 */ 426 public boolean isInternal() { 427 428 return true; 429 } 430 431 /** 432 * @see org.opencms.widgets.I_CmsWidget#newInstance() 433 */ 434 public I_CmsWidget newInstance() { 435 436 return new CmsHtmlWidget(getConfiguration()); 437 } 438 439 /** 440 * @see org.opencms.widgets.I_CmsWidget#setEditorValue(org.opencms.file.CmsObject, java.util.Map, org.opencms.widgets.I_CmsWidgetDialog, org.opencms.widgets.I_CmsWidgetParameter) 441 */ 442 @Override 443 public void setEditorValue( 444 CmsObject cms, 445 Map<String, String[]> formParameters, 446 I_CmsWidgetDialog widgetDialog, 447 I_CmsWidgetParameter param) { 448 449 String[] values = formParameters.get(param.getId()); 450 if ((values != null) && (values.length > 0)) { 451 String val = CmsEncoder.decode(values[0], CmsEncoder.ENCODING_UTF_8); 452 param.setStringValue(cms, val); 453 } 454 } 455 456 /** 457 * Adds the configuration for embedded gallery widgets the the JSON object.<p> 458 * 459 * @param result the JSON object to modify 460 * @param cms the OpenCms context 461 * @param schemaType the schema type 462 * @param messages the messages 463 * @param resource the edited resource 464 * @param contentLocale the content locale 465 * 466 * @throws JSONException in case JSON manipulation fails 467 */ 468 protected void addEmbeddedGalleryOptions( 469 JSONObject result, 470 CmsObject cms, 471 A_CmsXmlContentValue schemaType, 472 CmsMessages messages, 473 CmsResource resource, 474 Locale contentLocale) 475 throws JSONException { 476 477 CmsHtmlWidgetOption widgetOption = parseWidgetOptions(cms); 478 String embeddedImageGalleryOptions = widgetOption.getEmbeddedConfigurations().get("imagegallery"); 479 String embeddedDownloadGalleryOptions = widgetOption.getEmbeddedConfigurations().get("downloadgallery"); 480 481 if (embeddedDownloadGalleryOptions != null) { 482 CmsAdeDownloadGalleryWidget widget = new CmsAdeDownloadGalleryWidget(); 483 widget.setConfiguration(embeddedDownloadGalleryOptions); 484 String downloadJsonString = widget.getConfiguration( 485 cms, 486 schemaType/*?*/, 487 messages, 488 resource, 489 contentLocale); 490 491 JSONObject downloadJsonObj = new JSONObject(downloadJsonString); 492 filterEmbeddedGalleryOptions(downloadJsonObj); 493 result.put("downloadGalleryConfig", downloadJsonObj); 494 } 495 496 if (embeddedImageGalleryOptions != null) { 497 CmsAdeImageGalleryWidget widget = new CmsAdeImageGalleryWidget(); 498 widget.setConfiguration(embeddedImageGalleryOptions); 499 String imageJsonString = widget.getConfiguration(cms, schemaType/*?*/, messages, resource, contentLocale); 500 JSONObject imageJsonObj = new JSONObject(imageJsonString); 501 filterEmbeddedGalleryOptions(imageJsonObj); 502 result.put("imageGalleryConfig", imageJsonObj); 503 } 504 } 505 506 /** 507 * Returns the WYSIWYG editor configuration as a JSON object.<p> 508 * 509 * @param cms the OpenCms context 510 * @param resource the edited resource 511 * @param contentLocale the edited content locale 512 * 513 * @return the configuration 514 */ 515 protected JSONObject getJSONConfiguration(CmsObject cms, CmsResource resource, Locale contentLocale) { 516 517 return getJSONConfiguration(parseWidgetOptions(cms), cms, resource, contentLocale); 518 } 519 520 /** 521 * Removes all keys from the given JSON object which do not directly result from the embedded gallery configuration strings.<p> 522 * 523 * @param json the JSON object to modify 524 */ 525 private void filterEmbeddedGalleryOptions(JSONObject json) { 526 527 Set<String> validKeys = Sets.newHashSet( 528 Arrays.asList( 529 I_CmsGalleryProviderConstants.CONFIG_GALLERY_TYPES, 530 I_CmsGalleryProviderConstants.CONFIG_GALLERY_PATH, 531 I_CmsGalleryProviderConstants.CONFIG_USE_FORMATS, 532 I_CmsGalleryProviderConstants.CONFIG_IMAGE_FORMAT_NAMES, 533 I_CmsGalleryProviderConstants.CONFIG_IMAGE_FORMATS)); 534 535 // delete all keys not listed above 536 Set<String> toDelete = new HashSet<String>(Sets.difference(json.keySet(), validKeys)); 537 for (String toDeleteKey : toDelete) { 538 json.remove(toDeleteKey); 539 } 540 } 541 542 /** 543 * Returns the editor widget to use depending on the current users settings, current browser and installed editors.<p> 544 * 545 * @param cms the current CmsObject 546 * @param widgetDialog the dialog where the widget is used on 547 * @return the editor widget to use depending on the current users settings, current browser and installed editors 548 */ 549 private I_CmsWidget getEditorWidget(CmsObject cms, I_CmsWidgetDialog widgetDialog) { 550 551 if (m_editorWidget == null) { 552 // get HTML widget to use from editor manager 553 String widgetClassName = OpenCms.getWorkplaceManager().getWorkplaceEditorManager().getWidgetEditor( 554 cms.getRequestContext(), 555 widgetDialog.getUserAgent()); 556 boolean foundWidget = true; 557 if (CmsStringUtil.isEmpty(widgetClassName)) { 558 // no installed widget found, use default text area to edit HTML value 559 widgetClassName = CmsTextareaWidget.class.getName(); 560 foundWidget = false; 561 } 562 try { 563 if (foundWidget) { 564 // get widget instance and set the widget configuration 565 Class<?> widgetClass = Class.forName(widgetClassName); 566 A_CmsHtmlWidget editorWidget = (A_CmsHtmlWidget)widgetClass.newInstance(); 567 editorWidget.setConfiguration(getConfiguration()); 568 m_editorWidget = editorWidget; 569 } else { 570 // set the text area to display 15 rows for editing 571 Class<?> widgetClass = Class.forName(widgetClassName); 572 I_CmsWidget editorWidget = (I_CmsWidget)widgetClass.newInstance(); 573 editorWidget.setConfiguration("15"); 574 m_editorWidget = editorWidget; 575 } 576 } catch (Exception e) { 577 // failed to create widget instance 578 LOG.error( 579 Messages.get().container(Messages.LOG_CREATE_HTMLWIDGET_INSTANCE_FAILED_1, widgetClassName).key()); 580 } 581 582 } 583 return m_editorWidget; 584 } 585}