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}