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}