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