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