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.CmsFunctionReference;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.file.CmsVfsResourceNotFoundException;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsLog;
038import org.opencms.main.OpenCms;
039import org.opencms.security.CmsPermissionViolationException;
040import org.opencms.security.CmsSecurityException;
041import org.opencms.util.CmsFileUtil;
042import org.opencms.util.CmsStringUtil;
043import org.opencms.util.CmsUUID;
044
045import java.util.ArrayList;
046import java.util.Collection;
047import java.util.Collections;
048import java.util.List;
049import java.util.Locale;
050import java.util.function.BiPredicate;
051
052import org.apache.commons.logging.Log;
053
054/**
055 * This is a utility class which provides convenience methods for finding detail page names for resources which include
056 * the URL names of the resources themselves.<p>
057 *
058 * @see I_CmsDetailPageHandler
059 *
060 * @since 8.0.0
061 */
062public final class CmsDetailPageUtil {
063
064    /** The log object for this class. */
065    private static final Log LOG = CmsLog.getLog(CmsDetailPageUtil.class);
066
067    /**
068     * The hidden default constructor.<p>
069     */
070    private CmsDetailPageUtil() {
071
072        // do nothing
073    }
074
075    /**
076     * Gets a list of detail page URIs for the given resource, with its URL name appended.<p>
077     *
078     * @param cms the current CMS context
079     * @param res the resource for which the detail pages should be retrieved
080     *
081     * @return the list of detail page URIs
082     *
083     * @throws CmsException if something goes wrong
084     */
085    public static List<String> getAllDetailPagesWithUrlName(CmsObject cms, CmsResource res) throws CmsException {
086
087        List<String> result = new ArrayList<String>();
088        Collection<String> detailPages = OpenCms.getADEManager().getDetailPageHandler().getAllDetailPages(
089            cms,
090            res.getTypeId());
091        if (detailPages.isEmpty()) {
092            return Collections.<String> emptyList();
093        }
094        List<String> detailNames = cms.readUrlNamesForAllLocales(res.getStructureId());
095        for (String urlName : detailNames) {
096            for (String detailPage : detailPages) {
097                String rootPath = CmsStringUtil.joinPaths(detailPage, urlName, "/");
098                result.add(rootPath);
099            }
100        }
101        return result;
102    }
103
104    /**
105     * Returns either the newest URL name for a structure id, or  the structure id as a string if there is no URL name.<p>
106     *
107     * @param cms the current CMS context
108     * @param id the structure id of a resource
109     *
110     * @return the best URL name for the structure id
111     *
112     * @throws CmsException if something goes wrong
113     */
114    public static String getBestUrlName(CmsObject cms, CmsUUID id) throws CmsException {
115
116        // this is currently only used for static export
117        Locale locale = cms.getRequestContext().getLocale();
118        List<Locale> defaultLocales = OpenCms.getLocaleManager().getDefaultLocales();
119        String urlName = cms.readBestUrlName(id, locale, defaultLocales);
120        if (urlName != null) {
121            return urlName;
122        }
123        return id.toString();
124    }
125
126    /**
127     * Looks up a page by URI (which may be a detail page URI, or a normal VFS uri).<p>
128     *
129     * @param cms the current CMS context
130     * @param uri the detail page or VFS uri
131     *
132     * @return the resource with the given uri
133     *
134     * @throws CmsException if something goes wrong
135     */
136    public static CmsResource lookupPage(CmsObject cms, String uri) throws CmsException {
137
138        try {
139            CmsResource res = cms.readResource(uri);
140            return res;
141        } catch (CmsVfsResourceNotFoundException e) {
142            String detailName = CmsResource.getName(uri).replaceAll("/$", "");
143            CmsUUID detailId = cms.readIdForUrlName(detailName);
144            if (detailId != null) {
145                return cms.readResource(detailId);
146            }
147            throw new CmsVfsResourceNotFoundException(
148                org.opencms.db.generic.Messages.get().container(
149                    org.opencms.db.generic.Messages.ERR_READ_RESOURCE_1,
150                    uri));
151        }
152    }
153
154    /**
155     * Resolves a URI to the detail page that renders it and to the detail content it shows.<p>
156     *
157     * This is the resolution {@link CmsDetailPageResourceHandler} performs for a browser request, but
158     * without the servlet request and without any side effect: neither the request context nor any
159     * request attribute is changed, so the same lookup can be used outside the request cycle.<p>
160     *
161     * @param cms the CMS context, initialized with the site root the URI belongs to
162     * @param uri the site relative URI to resolve
163     *
164     * @return the detail resolution, or <code>null</code> if the URI is not a detail page URI
165     *
166     * @throws CmsException if something goes wrong
167     */
168    public static CmsDetailResolution resolveDetail(CmsObject cms, String uri) throws CmsException {
169
170        return resolveDetail(
171            cms,
172            uri,
173            (
174                page,
175                detailRes) -> OpenCms.getADEManager().getDetailPageHandler().isValidDetailPage(cms, page, detailRes));
176    }
177
178    /**
179     * Resolves a URI to the detail page that renders it and to the detail content it shows, using the
180     * given check for whether a detail page is valid for a detail content.<p>
181     *
182     * The check is a parameter because {@link CmsDetailPageResourceHandler#isValidDetailPage(CmsObject, CmsResource, CmsResource)}
183     * is an extension point of that handler; use {@link #resolveDetail(CmsObject, String)} to resolve with
184     * the standard check.<p>
185     *
186     * @param cms the CMS context, initialized with the site root the URI belongs to
187     * @param uri the site relative URI to resolve
188     * @param detailPageValidator checks whether a detail page (first argument) is valid for a detail content (second argument)
189     *
190     * @return the detail resolution, or <code>null</code> if the URI is not a detail page URI
191     *
192     * @throws CmsException if something goes wrong
193     */
194    public static CmsDetailResolution resolveDetail(
195        CmsObject cms,
196        String uri,
197        BiPredicate<CmsResource, CmsResource> detailPageValidator)
198    throws CmsException {
199
200        String path = CmsFileUtil.removeTrailingSeparator(uri);
201        try {
202            cms.readResource(path, CmsResourceFilter.IGNORE_EXPIRATION);
203        } catch (CmsSecurityException e) {
204            // It may happen that a path is both an existing VFS path and a valid detail page link.
205            // If this is the case, and the user has insufficient permissions to read the resource at the path,
206            // no resource should be displayed, even if the user would have access to the detail page.
207            return null;
208        } catch (CmsException e) {
209            // ignore
210        }
211        String detailName = CmsResource.getName(path);
212        try {
213            CmsUUID detailId = cms.readIdForUrlName(detailName);
214
215            if (detailId != null) {
216                // check existence / permissions
217                CmsResource detailRes = null;
218                CmsPermissionViolationException permissionDenied = null;
219                try {
220                    detailRes = cms.readResource(detailId, CmsResourceFilter.ignoreExpirationOffline(cms));
221                } catch (CmsPermissionViolationException e) {
222                    // we postpone the decision what to do with a permission violation until later (see below)
223                    permissionDenied = e;
224                }
225                String detailPagePath = CmsResource.getFolderPath(path);
226                CmsResource detailPage = cms.readDefaultFile(detailPagePath);
227                if (permissionDenied != null) {
228                    // If we got a permission violation while reading the detail content, we only want to rethrow it if the rest
229                    // of the URL is actually plausibly a detail page. Otherwise, we return null, which will usually cause a HTTP
230                    // 404 response status. This is to prevent broken links which accidentally end with a restricted detail content's
231                    // mapped URL name from triggering a HTTP 401 status. E.g. https://server.com/nonexistent-page/secret, where
232                    // there is no "nonexistent-page" folder and "secret" is the mapped URL name of a restricted content.
233                    if ((detailPage != null) && OpenCms.getADEManager().isDetailPage(cms, detailPage)) {
234                        throw permissionDenied;
235                    } else {
236                        LOG.debug(
237                            "Swallowing CmsPermissionViolationException for detail content because the page ["
238                                + detailPagePath
239                                + "] is not a detail page.\nDefault file: "
240                                + detailPage
241                                + "\n",
242                            permissionDenied);
243                        return null;
244                    }
245                }
246                // the page may be null when the folder of the URI has no default file; the detail page
247                // check dereferences the page, so this is checked here and reported as "no detail page"
248                if ((detailPage == null) || !detailPageValidator.test(detailPage, detailRes)) {
249                    return null;
250                }
251                return CmsDetailResolution.forContent(detailPage, detailRes, detailName);
252            }
253            CmsADEConfigData configData = OpenCms.getADEManager().lookupConfiguration(
254                cms,
255                cms.getRequestContext().addSiteRoot(path));
256            // check if the detail name matches any named function
257            for (CmsFunctionReference ref : configData.getFunctionReferences()) {
258                if (detailName.equals(ref.getName()) && (ref.getFunctionDefaultPageId() != null)) {
259                    CmsResource detailPage = cms.readDefaultFile(CmsResource.getFolderPath(path));
260                    if ((detailPage != null) && OpenCms.getADEManager().isDetailPage(cms, detailPage)) {
261                        return CmsDetailResolution.forFunction(
262                            detailPage,
263                            cms.readResource(ref.getFunctionDefaultPageId()),
264                            detailName);
265                    }
266                    return null;
267                }
268            }
269        } catch (CmsVfsResourceNotFoundException e) {
270            return null;
271        }
272        return null;
273    }
274}