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}