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, 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.ade.detailpage;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.ade.configuration.CmsADEManager;
032import org.opencms.configuration.CmsParameterConfiguration;
033import org.opencms.file.CmsObject;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsVfsResourceNotFoundException;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsLog;
038import org.opencms.main.OpenCms;
039import org.opencms.site.CmsSite;
040import org.opencms.util.CmsFileUtil;
041import org.opencms.util.CmsStringUtil;
042import org.opencms.workplace.CmsWorkplace;
043
044import java.util.ArrayList;
045import java.util.Arrays;
046import java.util.Collection;
047import java.util.List;
048import java.util.Set;
049
050import org.apache.commons.lang3.RandomStringUtils;
051import org.apache.commons.logging.Log;
052
053/**
054 * This class uses information from the detail page information stored in the sitemap to find/recognize the detail pages for
055 * a given resource.<p>
056 *
057 * @since 8.0.0
058 */
059public class CmsDefaultDetailPageHandler implements I_CmsDetailPageHandler {
060
061    /**
062     * Helper class for storing intermediate results when looking up detail pages for a resource.
063     */
064    private static class DetailPageConfigData {
065
066        /** The link source config. */
067        private CmsADEConfigData m_sourceConfig;
068
069        /** The configuration whose detail pages are used. */
070        private CmsADEConfigData m_configForDetailPages;
071
072        /** The link target configuration. */
073        private CmsADEConfigData m_targetConfig;
074
075        /** The detail pages. */
076        private List<CmsDetailPageInfo> m_detailPages = new ArrayList<>();
077
078        /**
079         * Gets the config for detail pages.
080         *
081         * @return the config for detail pages
082         */
083        public CmsADEConfigData getConfigForDetailPages() {
084
085            return m_configForDetailPages;
086        }
087
088        /**
089         * Gets the detail pages.
090         *
091         * @return the detail pages
092         */
093        public List<CmsDetailPageInfo> getDetailPages() {
094
095            return m_detailPages;
096        }
097
098        /**
099         * Gets the source config.
100         *
101         * @return the source config
102         */
103        @SuppressWarnings("unused")
104        public CmsADEConfigData getSourceConfig() {
105
106            return m_sourceConfig;
107        }
108
109        /**
110         * Gets the target config.
111         *
112         * @return the target config
113         */
114        @SuppressWarnings("unused")
115        public CmsADEConfigData getTargetConfig() {
116
117            return m_targetConfig;
118        }
119
120        /**
121         * Sets the config for detail pages.
122         *
123         * @param configForDetailPages the new config for detail pages
124         */
125        public void setConfigForDetailPages(CmsADEConfigData configForDetailPages) {
126
127            m_configForDetailPages = configForDetailPages;
128        }
129
130        /**
131         * Sets the detail pages.
132         *
133         * @param detailPages the new detail pages
134         */
135        public void setDetailPages(List<CmsDetailPageInfo> detailPages) {
136
137            m_detailPages = detailPages;
138        }
139
140        /**
141         * Sets the source config.
142         *
143         * @param sourceConfig the new source config
144         */
145        public void setSourceConfig(CmsADEConfigData sourceConfig) {
146
147            m_sourceConfig = sourceConfig;
148        }
149
150        /**
151         * Sets the target config.
152         *
153         * @param targetConfig the new target config
154         */
155        public void setTargetConfig(CmsADEConfigData targetConfig) {
156
157            m_targetConfig = targetConfig;
158        }
159
160    }
161
162    /** Logger instance for this class. */
163    private static final Log LOG = CmsLog.getLog(CmsDefaultDetailPageHandler.class);
164
165    /** The configuration. */
166    private CmsParameterConfiguration m_config = new CmsParameterConfiguration();
167
168    /**
169     * Constructor.
170     */
171    public CmsDefaultDetailPageHandler() {
172
173        // just for debugging
174        LOG.debug("Created default detail page handler.");
175    }
176
177    /**
178     * Adds the configuration parameter.
179     *
180     * @param paramName the param name
181     * @param paramValue the param value
182     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String)
183     */
184    public void addConfigurationParameter(String paramName, String paramValue) {
185
186        m_config.add(paramName, paramValue);
187
188    }
189
190    /**
191     * Gets the all detail pages.
192     *
193     * @param cms the cms
194     * @param resType the res type
195     * @return the all detail pages
196     * @throws CmsException the cms exception
197     * @see org.opencms.ade.detailpage.I_CmsDetailPageHandler#getAllDetailPages(org.opencms.file.CmsObject, int)
198     */
199    public Collection<String> getAllDetailPages(CmsObject cms, int resType) throws CmsException {
200
201        if (!OpenCms.getADEManager().isInitialized()) {
202            return new ArrayList<String>();
203        }
204        String typeName = OpenCms.getResourceManager().getResourceType(resType).getTypeName();
205        return OpenCms.getADEManager().getDetailPages(cms, typeName);
206    }
207
208    /**
209     * Gets the configuration.
210     *
211     * @return the configuration
212     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration()
213     */
214    public CmsParameterConfiguration getConfiguration() {
215
216        return m_config;
217
218    }
219
220    /**
221     * Gets the detail page for a content element.<p>
222     *
223     * @param manager the ADE manager instance.
224     * @param cms the CMS context
225     * @param contentRootPath the element's root path
226     * @param originPath the site path from which the detail content is being linked
227     * @param targetDetailPage the target detail page to use
228     *
229     * @return the detail page for the content element
230     */
231    public String getDetailPage(
232        CmsADEManager manager,
233        CmsObject cms,
234        String contentRootPath,
235        String originPath,
236        String targetDetailPage) {
237
238        boolean online = cms.getRequestContext().getCurrentProject().isOnlineProject();
239        Set<String> parentFolderTypes = manager.getParentFolderTypes(online, contentRootPath);
240        if (parentFolderTypes.isEmpty()) {
241            return null;
242        }
243
244        if (targetDetailPage != null) {
245            try {
246                CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(targetDetailPage);
247                CmsResource targetDetailPageRes = null;
248                if (site != null) {
249                    CmsObject rootCms = OpenCms.initCmsObject(cms);
250                    rootCms.getRequestContext().setSiteRoot("");
251                    targetDetailPageRes = rootCms.readResource(targetDetailPage);
252                } else {
253                    targetDetailPageRes = cms.readResource(targetDetailPage);
254                }
255                if (manager.isDetailPage(cms, targetDetailPageRes)) {
256                    return targetDetailPageRes.getRootPath();
257                }
258            } catch (CmsVfsResourceNotFoundException e) {
259                LOG.debug(e.getLocalizedMessage(), e);
260            } catch (Exception e) {
261                LOG.warn(e.getLocalizedMessage(), e);
262            }
263        }
264
265        String resType = null;
266        try {
267            CmsObject rootCms = OpenCms.initCmsObject(cms);
268            rootCms.getRequestContext().setSiteRoot("");
269            CmsResource detailResource = rootCms.readResource(contentRootPath);
270            resType = OpenCms.getResourceManager().getResourceType(detailResource).getTypeName();
271        } catch (CmsVfsResourceNotFoundException e) {
272            LOG.info(e.getLocalizedMessage(), e);
273        } catch (Exception e) {
274            LOG.warn(e.getLocalizedMessage(), e);
275        }
276        if (resType == null) {
277            if (parentFolderTypes.size() > 1) {
278                LOG.warn(
279                    "Couldn't determine resource type of detail resource "
280                        + contentRootPath
281                        + ", picking arbitrarily from configured parent folder types "
282                        + parentFolderTypes);
283            }
284            resType = parentFolderTypes.iterator().next();
285
286        }
287        DetailPageConfigData context = lookupDetailPageConfigData(manager, cms, contentRootPath, originPath, resType);
288        List<CmsDetailPageInfo> relevantPages = context.getDetailPages();
289        if (context.getConfigForDetailPages() == null) {
290            return null;
291        }
292        LOG.info(
293            "Trying to determine detail page for '"
294                + contentRootPath
295                + "' in context '"
296                + context.getConfigForDetailPages().getBasePath()
297                + "'");
298        if (!CmsStringUtil.isPrefixPath(
299            context.getConfigForDetailPages().getExternalDetailContentExclusionFolder(),
300            contentRootPath)) {
301            return null;
302        }
303        String result = new CmsDetailPageFilter(cms, contentRootPath).filterDetailPages(relevantPages).map(
304            info -> info.getUri()).findFirst().orElse(null);
305        return result;
306    }
307
308    /**
309     * Gets the detail page to use for a detail resource.
310     *
311     * @param cms the cms
312     * @param rootPath the root path
313     * @param linkSource the link source
314     * @param targetDetailPage the target detail page
315     * @return the detail page
316     * @see org.opencms.ade.detailpage.I_CmsDetailPageHandler#getDetailPage(org.opencms.file.CmsObject, java.lang.String, java.lang.String, java.lang.String)
317     */
318    public String getDetailPage(CmsObject cms, String rootPath, String linkSource, String targetDetailPage) {
319
320        CmsADEManager manager = OpenCms.getADEManager();
321        if (!manager.isInitialized()) {
322            return null;
323        }
324
325        if (rootPath.endsWith(".jsp") || rootPath.startsWith(CmsWorkplace.VFS_PATH_WORKPLACE)) {
326            // exclude these for performance reasons
327            return null;
328        }
329        String result = getDetailPage(manager, cms, rootPath, linkSource, targetDetailPage);
330        if (result == null) {
331            return null;
332        }
333        if (!CmsResource.isFolder(result)) {
334            result = CmsResource.getFolderPath(result);
335        }
336        return result;
337    }
338
339    /**
340     * Inits the configuration.
341     *
342     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration()
343     */
344    public void initConfiguration() {
345
346        m_config = CmsParameterConfiguration.unmodifiableVersion(m_config);
347
348    }
349
350    /**
351     * Initialize.
352     *
353     * @param offlineCms the offline cms
354     * @param onlineCms the online cms
355     * @see org.opencms.ade.detailpage.I_CmsDetailPageHandler#initialize(org.opencms.file.CmsObject, org.opencms.file.CmsObject)
356     */
357    public void initialize(CmsObject offlineCms, CmsObject onlineCms) {
358
359        // do nothing
360    }
361
362    /**
363     * Checks whether the given detail page is valid for the given resource.<p>
364     *
365     * @param cms the CMS context
366     * @param page the detail page
367     * @param detailRes the detail resource
368     *
369     * @return true if the given detail page is valid
370     */
371    public boolean isValidDetailPage(CmsObject cms, CmsResource page, CmsResource detailRes) {
372
373        String p = "[" + RandomStringUtils.randomAlphanumeric(6) + "] ";
374        LOG.debug(p + "isValidDetailPage(" + page.getRootPath() + "," + detailRes.getRootPath() + ")");
375        if (OpenCms.getSystemInfo().isRestrictDetailContents()) {
376            // in 'restrict detail contents mode', do not allow detail contents from a real site on a detail page of a different real site
377            CmsSite pageSite = OpenCms.getSiteManager().getSiteForRootPath(page.getRootPath());
378            CmsSite detailSite = OpenCms.getSiteManager().getSiteForRootPath(detailRes.getRootPath());
379            if ((pageSite != null)
380                && (detailSite != null)
381                && !pageSite.getSiteRoot().equals(detailSite.getSiteRoot())) {
382                LOG.debug(p + "returned false because of restrict-detail-contents option");
383                return false;
384            }
385        }
386
387        if (!OpenCms.getADEManager().isDetailPage(cms, page)) {
388            LOG.debug(p + "returned false because the page is not a detail page.");
389            return false;
390        }
391
392        String typeName = OpenCms.getResourceManager().getResourceType(detailRes).getTypeName();
393        DetailPageConfigData context = lookupDetailPageConfigData(
394            OpenCms.getADEManager(),
395            cms,
396            detailRes.getRootPath(),
397            cms.getSitePath(page),
398            typeName);
399        String pageFolder = CmsFileUtil.removeTrailingSeparator(CmsResource.getParentFolder(page.getRootPath()));
400        CmsDetailPageFilter detailPageFilter = new CmsDetailPageFilter(cms, detailRes);
401        boolean foundDetailPage = detailPageFilter.filterDetailPages(context.getDetailPages()).anyMatch(
402            info -> pageFolder.equals(CmsFileUtil.removeTrailingSeparator(info.getUri())));
403        CmsADEConfigData configForPage = context.getConfigForDetailPages();
404        if (configForPage == null) {
405            LOG.debug(p + "Returned false because no valid sitemap configuration found");
406            return false;
407        }
408        if (!foundDetailPage) {
409            LOG.debug(p + "Returned false because detail page is not in context " + configForPage.getBasePath());
410            return false;
411        }
412        if (!CmsStringUtil.isPrefixPath(
413            configForPage.getExternalDetailContentExclusionFolder(),
414            detailRes.getRootPath())) {
415            LOG.debug(
416                p
417                    + "returned false because of external detail content exclusion folder "
418                    + configForPage.getExternalDetailContentExclusionFolder());
419            return false;
420        }
421        return true;
422    }
423
424    /**
425     * Gets the context.
426     *
427     * @param manager the manager
428     * @param cms the cms
429     * @param contentPath the content path
430     * @param originPath the origin path
431     * @param resType the res type
432     * @return the context
433     */
434    private DetailPageConfigData lookupDetailPageConfigData(
435        CmsADEManager manager,
436        CmsObject cms,
437        String contentPath,
438        String originPath,
439        String resType) {
440
441        DetailPageConfigData context = new DetailPageConfigData();
442
443        CmsADEConfigData configData = manager.lookupConfigurationWithCache(
444            cms,
445            cms.getRequestContext().addSiteRoot(originPath));
446        context.setSourceConfig(configData);
447        CmsADEConfigData targetConfigData = manager.lookupConfigurationWithCache(cms, contentPath);
448        context.setTargetConfig(targetConfigData);
449        boolean targetFirst = targetConfigData.isPreferDetailPagesForLocalContents();
450        List<CmsADEConfigData> configs = targetFirst
451        ? Arrays.asList(targetConfigData, configData)
452        : Arrays.asList(configData, targetConfigData);
453        for (CmsADEConfigData config : configs) {
454            List<CmsDetailPageInfo> pageInfo = config.getDetailPagesForType(resType);
455            if ((pageInfo != null) && !pageInfo.isEmpty()) {
456                context.setConfigForDetailPages(config);
457                context.setDetailPages(pageInfo);
458                break;
459            }
460        }
461        return context;
462    }
463
464}