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 String editorHeight = widgetOptions.getEditorHeight(); 144 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(editorHeight)) { 145 editorHeight = editorHeight.replaceAll("px", ""); 146 result.put("height", editorHeight); 147 } 148 // set CSS style sheet for current editor widget if configured 149 boolean cssConfigured = false; 150 String cssPath = ""; 151 if (widgetOptions.useCss()) { 152 cssPath = widgetOptions.getCssPath(); 153 // set the CSS path to null (the created configuration String passed to JS will not include this path then) 154 widgetOptions.setCssPath(null); 155 cssConfigured = true; 156 } else if (OpenCms.getWorkplaceManager().getEditorCssHandlers().size() > 0) { 157 Iterator<I_CmsEditorCssHandler> i = OpenCms.getWorkplaceManager().getEditorCssHandlers().iterator(); 158 try { 159 String editedResourceSitePath = resource == null ? null : cms.getSitePath(resource); 160 while (i.hasNext()) { 161 I_CmsEditorCssHandler handler = i.next(); 162 if (handler.matches(cms, editedResourceSitePath)) { 163 cssPath = handler.getUriStyleSheet(cms, editedResourceSitePath); 164 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(cssPath)) { 165 cssConfigured = true; 166 } 167 break; 168 } 169 } 170 } catch (Exception e) { 171 // ignore, CSS could not be set 172 } 173 } 174 if (cssConfigured) { 175 result.put("content_css", OpenCms.getLinkManager().substituteLink(cms, cssPath)); 176 } 177 178 if (widgetOptions.showStylesFormat()) { 179 try { 180 CmsFile file = cms.readFile(widgetOptions.getStylesFormatPath()); 181 String characterEncoding = OpenCms.getSystemInfo().getDefaultEncoding(); 182 result.put("style_formats", new String(file.getContents(), characterEncoding)); 183 } catch (CmsException cmsException) { 184 LOG.error("Can not open file:" + widgetOptions.getStylesFormatPath(), cmsException); 185 } catch (UnsupportedEncodingException ex) { 186 LOG.error(ex); 187 } 188 } 189 if (widgetOptions.isImportCss()) { 190 result.put("importCss", true); 191 } 192 String formatSelectOptions = widgetOptions.getFormatSelectOptions(); 193 if (!CmsStringUtil.isEmpty(formatSelectOptions) 194 && !widgetOptions.isButtonHidden(CmsHtmlWidgetOption.OPTION_FORMATSELECT)) { 195 result.put("block_formats", getTinyMceBlockFormats(formatSelectOptions)); 196 } 197 Boolean pasteText = Boolean.valueOf( 198 OpenCms.getWorkplaceManager().getWorkplaceEditorManager().getEditorParameter( 199 cms, 200 "tinymce", 201 "paste_text")); 202 JSONObject pasteOptions = new JSONObject(); 203 pasteOptions.put("paste_text_sticky_default", pasteText); 204 pasteOptions.put("paste_text_sticky", pasteText); 205 result.put("pasteOptions", pasteOptions); 206 // if spell checking is enabled, add the spell handler URL 207 if (OpenCmsSpellcheckHandler.isSpellcheckingEnabled()) { 208 result.put( 209 "spellcheck_url", 210 OpenCms.getLinkManager().substituteLinkForUnknownTarget( 211 cms, 212 OpenCmsSpellcheckHandler.getSpellcheckHandlerPath())); 213 214 result.put("spellcheck_language", contentLocale.getLanguage()); 215 } 216 217 String linkDefaultProtocol = widgetOptions.getLinkDefaultProtocol(); 218 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(linkDefaultProtocol)) { 219 result.put("link_default_protocol", linkDefaultProtocol); 220 } 221 222 String editorOptions = widgetOptions.getEditorConfigPath(); 223 if (editorOptions != null) { 224 try { 225 CmsResource editorOptionsRes = cms.readResource(editorOptions, CmsResourceFilter.IGNORE_EXPIRATION); 226 CmsFile editorOptionsFile = cms.readFile(editorOptionsRes); 227 OpenCms.getLocaleManager(); 228 String encoding = CmsLocaleManager.getResourceEncoding(cms, editorOptionsRes); 229 String contentAsString = new String(editorOptionsFile.getContents(), encoding); 230 JSONObject directOptions = new JSONObject(contentAsString); 231 // JSON may contain user-readable strings, which we may want to localize, 232 // but we also don't want to accidentally produce invalid JSON, so we recursively 233 // replace macros in string values occuring in the JSON 234 CmsMacroResolver resolver = new CmsMacroResolver(); 235 resolver.setCmsObject(cms); 236 resolver.setMessages(OpenCms.getWorkplaceManager().getMessages(workplaceLocale)); 237 JSONObject replacedOptions = CmsJsonUtil.mapJsonObject(directOptions, val -> { 238 if (val instanceof String) { 239 return resolver.resolveMacros((String)val); 240 } else { 241 return val; 242 } 243 }); 244 result.put("directOptions", replacedOptions); 245 } catch (Exception e) { 246 LOG.error( 247 "Error processing editor options from " + editorOptions + ": " + e.getLocalizedMessage(), 248 e); 249 } 250 } 251 252 } catch (JSONException e) { 253 LOG.error(e.getLocalizedMessage(), e); 254 } 255 return result; 256 } 257 258 /** 259 * Gets the block format configuration string for TinyMCE from the configured format select options.<p> 260 * 261 * @param formatSelectOptions the format select options 262 * 263 * @return the block_formats configuration 264 */ 265 public static String getTinyMceBlockFormats(String formatSelectOptions) { 266 267 String[] options = formatSelectOptions.split(";"); 268 List<String> resultParts = Lists.newArrayList(); 269 for (String option : options) { 270 String label = TINYMCE_DEFAULT_BLOCK_FORMAT_LABELS.get(option); 271 if (label == null) { 272 label = option; 273 } 274 resultParts.add(label + "=" + option); 275 } 276 String result = CmsStringUtil.listAsString(resultParts, ";"); 277 return result; 278 } 279 280 /** 281 * @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) 282 */ 283 public String getConfiguration( 284 CmsObject cms, 285 A_CmsXmlContentValue schemaType, 286 CmsMessages messages, 287 CmsResource resource, 288 Locale contentLocale) { 289 290 JSONObject result = getJSONConfiguration(cms, resource, contentLocale); 291 try { 292 addEmbeddedGalleryOptions(result, cms, schemaType, messages, resource, contentLocale); 293 } catch (JSONException e) { 294 LOG.error(e.getLocalizedMessage(), e); 295 } 296 return result.toString(); 297 } 298 299 /** 300 * @see org.opencms.widgets.I_CmsADEWidget#getCssResourceLinks(org.opencms.file.CmsObject) 301 */ 302 public List<String> getCssResourceLinks(CmsObject cms) { 303 304 // not needed for internal widget 305 return null; 306 } 307 308 /** 309 * @see org.opencms.widgets.I_CmsADEWidget#getDefaultDisplayType() 310 */ 311 public DisplayType getDefaultDisplayType() { 312 313 return DisplayType.wide; 314 } 315 316 /** 317 * @see org.opencms.widgets.I_CmsWidget#getDialogIncludes(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog) 318 */ 319 @Override 320 public String getDialogIncludes(CmsObject cms, I_CmsWidgetDialog widgetDialog) { 321 322 return getEditorWidget(cms, widgetDialog).getDialogIncludes(cms, widgetDialog); 323 } 324 325 /** 326 * @see org.opencms.widgets.I_CmsWidget#getDialogInitCall(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog) 327 */ 328 @Override 329 public String getDialogInitCall(CmsObject cms, I_CmsWidgetDialog widgetDialog) { 330 331 return getEditorWidget(cms, widgetDialog).getDialogInitCall(cms, widgetDialog); 332 } 333 334 /** 335 * @see org.opencms.widgets.I_CmsWidget#getDialogInitMethod(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog) 336 */ 337 @Override 338 public String getDialogInitMethod(CmsObject cms, I_CmsWidgetDialog widgetDialog) { 339 340 return getEditorWidget(cms, widgetDialog).getDialogInitMethod(cms, widgetDialog); 341 } 342 343 /** 344 * @see org.opencms.widgets.I_CmsWidget#getDialogWidget(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog, org.opencms.widgets.I_CmsWidgetParameter) 345 */ 346 public String getDialogWidget(CmsObject cms, I_CmsWidgetDialog widgetDialog, I_CmsWidgetParameter param) { 347 348 return getEditorWidget(cms, widgetDialog).getDialogWidget(cms, widgetDialog, param); 349 } 350 351 /** 352 * @see org.opencms.widgets.I_CmsADEWidget#getInitCall() 353 */ 354 public String getInitCall() { 355 356 // not needed for internal widget 357 return null; 358 } 359 360 /** 361 * @see org.opencms.widgets.I_CmsADEWidget#getJavaScriptResourceLinks(org.opencms.file.CmsObject) 362 */ 363 public List<String> getJavaScriptResourceLinks(CmsObject cms) { 364 365 // not needed for internal widget 366 return null; 367 } 368 369 /** 370 * @see org.opencms.widgets.I_CmsADEWidget#getWidgetName() 371 */ 372 public String getWidgetName() { 373 374 return CmsHtmlWidget.class.getName(); 375 } 376 377 /** 378 * @see org.opencms.widgets.I_CmsADEWidget#isInternal() 379 */ 380 public boolean isInternal() { 381 382 return true; 383 } 384 385 /** 386 * @see org.opencms.widgets.I_CmsWidget#newInstance() 387 */ 388 public I_CmsWidget newInstance() { 389 390 return new CmsHtmlWidget(getConfiguration()); 391 } 392 393 /** 394 * @see org.opencms.widgets.I_CmsWidget#setEditorValue(org.opencms.file.CmsObject, java.util.Map, org.opencms.widgets.I_CmsWidgetDialog, org.opencms.widgets.I_CmsWidgetParameter) 395 */ 396 @Override 397 public void setEditorValue( 398 CmsObject cms, 399 Map<String, String[]> formParameters, 400 I_CmsWidgetDialog widgetDialog, 401 I_CmsWidgetParameter param) { 402 403 String[] values = formParameters.get(param.getId()); 404 if ((values != null) && (values.length > 0)) { 405 String val = CmsEncoder.decode(values[0], CmsEncoder.ENCODING_UTF_8); 406 param.setStringValue(cms, val); 407 } 408 } 409 410 /** 411 * Adds the configuration for embedded gallery widgets the the JSON object.<p> 412 * 413 * @param result the JSON object to modify 414 * @param cms the OpenCms context 415 * @param schemaType the schema type 416 * @param messages the messages 417 * @param resource the edited resource 418 * @param contentLocale the content locale 419 * 420 * @throws JSONException in case JSON manipulation fails 421 */ 422 protected void addEmbeddedGalleryOptions( 423 JSONObject result, 424 CmsObject cms, 425 A_CmsXmlContentValue schemaType, 426 CmsMessages messages, 427 CmsResource resource, 428 Locale contentLocale) 429 throws JSONException { 430 431 CmsHtmlWidgetOption widgetOption = parseWidgetOptions(cms); 432 String embeddedImageGalleryOptions = widgetOption.getEmbeddedConfigurations().get("imagegallery"); 433 String embeddedDownloadGalleryOptions = widgetOption.getEmbeddedConfigurations().get("downloadgallery"); 434 435 if (embeddedDownloadGalleryOptions != null) { 436 CmsAdeDownloadGalleryWidget widget = new CmsAdeDownloadGalleryWidget(); 437 widget.setConfiguration(embeddedDownloadGalleryOptions); 438 String downloadJsonString = widget.getConfiguration( 439 cms, 440 schemaType/*?*/, 441 messages, 442 resource, 443 contentLocale); 444 445 JSONObject downloadJsonObj = new JSONObject(downloadJsonString); 446 filterEmbeddedGalleryOptions(downloadJsonObj); 447 result.put("downloadGalleryConfig", downloadJsonObj); 448 } 449 450 if (embeddedImageGalleryOptions != null) { 451 CmsAdeImageGalleryWidget widget = new CmsAdeImageGalleryWidget(); 452 widget.setConfiguration(embeddedImageGalleryOptions); 453 String imageJsonString = widget.getConfiguration(cms, schemaType/*?*/, messages, resource, contentLocale); 454 JSONObject imageJsonObj = new JSONObject(imageJsonString); 455 filterEmbeddedGalleryOptions(imageJsonObj); 456 result.put("imageGalleryConfig", imageJsonObj); 457 } 458 } 459 460 /** 461 * Returns the WYSIWYG editor configuration as a JSON object.<p> 462 * 463 * @param cms the OpenCms context 464 * @param resource the edited resource 465 * @param contentLocale the edited content locale 466 * 467 * @return the configuration 468 */ 469 protected JSONObject getJSONConfiguration(CmsObject cms, CmsResource resource, Locale contentLocale) { 470 471 return getJSONConfiguration(parseWidgetOptions(cms), cms, resource, contentLocale); 472 } 473 474 /** 475 * Removes all keys from the given JSON object which do not directly result from the embedded gallery configuration strings.<p> 476 * 477 * @param json the JSON object to modify 478 */ 479 private void filterEmbeddedGalleryOptions(JSONObject json) { 480 481 Set<String> validKeys = Sets.newHashSet( 482 Arrays.asList( 483 I_CmsGalleryProviderConstants.CONFIG_GALLERY_TYPES, 484 I_CmsGalleryProviderConstants.CONFIG_GALLERY_PATH, 485 I_CmsGalleryProviderConstants.CONFIG_USE_FORMATS, 486 I_CmsGalleryProviderConstants.CONFIG_IMAGE_FORMAT_NAMES, 487 I_CmsGalleryProviderConstants.CONFIG_IMAGE_FORMATS)); 488 489 // delete all keys not listed above 490 Set<String> toDelete = new HashSet<String>(Sets.difference(json.keySet(), validKeys)); 491 for (String toDeleteKey : toDelete) { 492 json.remove(toDeleteKey); 493 } 494 } 495 496 /** 497 * Returns the editor widget to use depending on the current users settings, current browser and installed editors.<p> 498 * 499 * @param cms the current CmsObject 500 * @param widgetDialog the dialog where the widget is used on 501 * @return the editor widget to use depending on the current users settings, current browser and installed editors 502 */ 503 private I_CmsWidget getEditorWidget(CmsObject cms, I_CmsWidgetDialog widgetDialog) { 504 505 if (m_editorWidget == null) { 506 // get HTML widget to use from editor manager 507 String widgetClassName = OpenCms.getWorkplaceManager().getWorkplaceEditorManager().getWidgetEditor( 508 cms.getRequestContext(), 509 widgetDialog.getUserAgent()); 510 boolean foundWidget = true; 511 if (CmsStringUtil.isEmpty(widgetClassName)) { 512 // no installed widget found, use default text area to edit HTML value 513 widgetClassName = CmsTextareaWidget.class.getName(); 514 foundWidget = false; 515 } 516 try { 517 if (foundWidget) { 518 // get widget instance and set the widget configuration 519 Class<?> widgetClass = Class.forName(widgetClassName); 520 A_CmsHtmlWidget editorWidget = (A_CmsHtmlWidget)widgetClass.newInstance(); 521 editorWidget.setConfiguration(getConfiguration()); 522 m_editorWidget = editorWidget; 523 } else { 524 // set the text area to display 15 rows for editing 525 Class<?> widgetClass = Class.forName(widgetClassName); 526 I_CmsWidget editorWidget = (I_CmsWidget)widgetClass.newInstance(); 527 editorWidget.setConfiguration("15"); 528 m_editorWidget = editorWidget; 529 } 530 } catch (Exception e) { 531 // failed to create widget instance 532 LOG.error( 533 Messages.get().container(Messages.LOG_CREATE_HTMLWIDGET_INSTANCE_FAILED_1, widgetClassName).key()); 534 } 535 536 } 537 return m_editorWidget; 538 } 539}