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.repository;
029
030import org.opencms.file.CmsFile;
031import org.opencms.file.CmsProperty;
032import org.opencms.file.CmsResource;
033import org.opencms.file.CmsResourceFilter;
034import org.opencms.file.CmsUser;
035import org.opencms.file.CmsVfsResourceAlreadyExistsException;
036import org.opencms.file.CmsVfsResourceNotFoundException;
037import org.opencms.file.types.A_CmsResourceTypeFolderBase;
038import org.opencms.file.types.CmsResourceTypeFolder;
039import org.opencms.file.wrapper.CmsObjectWrapper;
040import org.opencms.lock.CmsLock;
041import org.opencms.main.CmsException;
042import org.opencms.main.CmsLog;
043import org.opencms.main.OpenCms;
044import org.opencms.security.CmsSecurityException;
045import org.opencms.util.CmsFileUtil;
046import org.opencms.util.CmsResourceTranslator;
047import org.opencms.util.CmsStringUtil;
048
049import java.io.IOException;
050import java.io.InputStream;
051import java.util.ArrayList;
052import java.util.HashMap;
053import java.util.Iterator;
054import java.util.List;
055import java.util.Map;
056
057import org.apache.commons.logging.Log;
058
059import com.google.common.io.BaseEncoding;
060
061/**
062 * This is the session class to work with the {@link CmsRepository}.<p>
063 *
064 * You can get an instance of this class by calling
065 * {@link CmsRepository#login(String, String)}.<p>
066 *
067 * This class provides basic file and folder operations on the resources
068 * in the VFS of OpenCms.<p>
069 *
070 * @see A_CmsRepositorySession
071 * @see I_CmsRepositorySession
072 *
073 * @since 6.5.6
074 */
075public class CmsRepositorySession extends A_CmsRepositorySession {
076
077    /** The log object for this class. */
078    private static final Log LOG = CmsLog.getLog(CmsRepositorySession.class);
079
080    /** Default namespace for OpenCms properties. */
081    public static final String PROPERTY_NAMESPACE = "http://opencms.org/ns/property";
082
083    /** Prefix used for encoded property names outside the default property namespace. */
084    public static final String EXTERNAL_PREFIX = "xDAV_";
085
086    /** Base for the namespace encoding. */
087    private static final BaseEncoding PROPERTY_NS_CODEC = BaseEncoding.base64Url().withPadChar('$');
088
089    /** Repository-specific file translations to use (may be null). */
090    private CmsResourceTranslator m_translation;
091
092    /** The initialized {@link CmsObjectWrapper}. */
093    private final CmsObjectWrapper m_cms;
094
095    /**
096     * Constructor with an initialized {@link CmsObjectWrapper} and a
097     * {@link CmsRepositoryFilter} to use.<p>
098     *
099     * @param cms the initialized CmsObject
100     * @param filter the repository filter to use
101     * @param translation the repository specific file translations (may be null)
102     */
103    public CmsRepositorySession(CmsObjectWrapper cms, CmsRepositoryFilter filter, CmsResourceTranslator translation) {
104
105        m_cms = cms;
106        setFilter(filter);
107        m_translation = translation;
108    }
109
110    /**
111     * Decodes the namespace URI.
112     *
113     * @param code the encoded namespace URI
114     * @return the decoded namespace URI
115     * @throws Exception if something goes wrong
116     */
117    public static String decodeNamespace(String code) throws Exception {
118
119        code = code.replace("~", "_");
120        return new String(PROPERTY_NS_CODEC.decode(code), "UTF-8");
121    }
122
123    /**
124     * Encodes the namespace URI.
125     *
126     * @param data a namesapce URI
127     * @return the encoded namespace URI
128     *
129     * @throws Exception if something goes wrong
130     */
131    public static String encodeNamespace(String data) throws Exception {
132
133        String s = BaseEncoding.base64Url().withPadChar('$').encode(data.getBytes("UTF-8"));
134        s = s.replace('_', '~');
135        return s;
136    }
137
138    /**
139     * @see org.opencms.repository.I_CmsRepositorySession#copy(java.lang.String, java.lang.String, boolean)
140     */
141    public void copy(String src, String dest, boolean overwrite, boolean shallow) throws CmsException {
142
143        src = validatePath(src);
144        dest = validatePath(dest);
145
146        if (LOG.isDebugEnabled()) {
147            LOG.debug(Messages.get().getBundle().key(Messages.LOG_COPY_ITEM_2, src, dest));
148        }
149
150        // It is only possible in OpenCms to overwrite files.
151        // Folder are not possible to overwrite.
152        if (exists(dest)) {
153
154            if (overwrite) {
155                CmsResource srcRes = m_cms.readResource(src, CmsResourceFilter.DEFAULT);
156                CmsResource destRes = m_cms.readResource(dest, CmsResourceFilter.DEFAULT);
157
158                if ((srcRes.isFile()) && (destRes.isFile())) {
159
160                    if (LOG.isDebugEnabled()) {
161                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DELETE_DEST_0));
162                    }
163
164                    // delete existing resource
165                    delete(dest);
166                } else {
167
168                    if (LOG.isDebugEnabled()) {
169                        LOG.debug(Messages.get().getBundle().key(Messages.ERR_OVERWRITE_0));
170                    }
171
172                    // internal error (not possible)
173                    throw new CmsException(Messages.get().container(Messages.ERR_OVERWRITE_0));
174                }
175            } else {
176
177                if (LOG.isDebugEnabled()) {
178                    LOG.debug(Messages.get().getBundle().key(Messages.ERR_DEST_EXISTS_0));
179                }
180
181                throw new CmsVfsResourceAlreadyExistsException(Messages.get().container(Messages.ERR_DEST_EXISTS_0));
182            }
183        }
184
185        // copy resource
186        if (shallow) {
187            m_cms.getRequestContext().setAttribute(A_CmsResourceTypeFolderBase.ATTR_SHALLOW_FOLDER_COPY, Boolean.TRUE);
188        }
189        try {
190            m_cms.copyResource(src, dest, CmsResource.COPY_PRESERVE_SIBLING);
191        } finally {
192            m_cms.getRequestContext().removeAttribute(A_CmsResourceTypeFolderBase.ATTR_SHALLOW_FOLDER_COPY);
193        }
194
195        // unlock destination resource
196        m_cms.unlockResource(dest);
197    }
198
199    /**
200     * @see org.opencms.repository.I_CmsRepositorySession#create(java.lang.String)
201     */
202    public void create(String path) throws CmsException {
203
204        path = validatePath(path);
205
206        if (LOG.isDebugEnabled()) {
207            LOG.debug(Messages.get().getBundle().key(Messages.LOG_CREATE_ITEM_1, path));
208        }
209
210        // create the folder
211        CmsResource res = m_cms.createResource(path, CmsResourceTypeFolder.RESOURCE_TYPE_ID);
212
213        // unlock new created folders if lock is not inherited
214        if (!m_cms.getLock(res).isInherited()) {
215            m_cms.unlockResource(path);
216        }
217    }
218
219    /**
220     * @see org.opencms.repository.I_CmsRepositorySession#delete(java.lang.String)
221     */
222    public void delete(String path) throws CmsException {
223
224        path = validatePath(path);
225
226        if (LOG.isDebugEnabled()) {
227            LOG.debug(Messages.get().getBundle().key(Messages.LOG_DELETE_ITEM_1, path));
228        }
229
230        CmsRepositoryLockInfo lock = getLock(path);
231
232        // lock resource
233        m_cms.lockResource(path);
234
235        // delete resource
236        m_cms.deleteResource(path, CmsResource.DELETE_PRESERVE_SIBLINGS);
237
238        // if deleting items out of a xml page restore lock state after deleting
239        try {
240            if (lock == null) {
241                m_cms.unlockResource(path);
242            }
243        } catch (CmsException ex) {
244            // noop
245        }
246    }
247
248    /**
249     * @see org.opencms.repository.I_CmsRepositorySession#exists(java.lang.String)
250     */
251    public boolean exists(String path) {
252
253        try {
254            path = validatePath(path);
255            return m_cms.existsResource(path);
256        } catch (CmsException ex) {
257            return false;
258        }
259    }
260
261    /**
262     * @see org.opencms.repository.I_CmsRepositorySession#getItem(java.lang.String)
263     */
264    public I_CmsRepositoryItem getItem(String path) throws CmsException {
265
266        path = validatePath(path);
267
268        if (LOG.isDebugEnabled()) {
269            LOG.debug(Messages.get().getBundle().key(Messages.LOG_READ_ITEM_1, path));
270        }
271
272        CmsResource res = m_cms.readResource(path, CmsResourceFilter.DEFAULT);
273
274        CmsRepositoryItem item = new CmsRepositoryItem(res, m_cms);
275        return item;
276    }
277
278    /**
279     * @see org.opencms.repository.I_CmsRepositorySession#getLock(java.lang.String)
280     */
281    public CmsRepositoryLockInfo getLock(String path) {
282
283        try {
284            CmsRepositoryLockInfo lockInfo = new CmsRepositoryLockInfo();
285
286            path = validatePath(path);
287
288            CmsResource res = m_cms.readResource(path, CmsResourceFilter.DEFAULT);
289
290            // check user locks
291            CmsLock cmsLock = m_cms.getLock(res);
292            if (!cmsLock.isUnlocked()) {
293                lockInfo.setPath(path);
294
295                CmsUser owner = m_cms.readUser(cmsLock.getUserId());
296                if (owner != null) {
297                    lockInfo.setUsername(owner.getName());
298                    lockInfo.setOwner(owner.getName() + "||" + owner.getEmail());
299                }
300                return lockInfo;
301            }
302
303            return null;
304        } catch (CmsException ex) {
305
306            // error occurred while finding locks
307            // return null (no lock found)
308            return null;
309        }
310    }
311
312    /**
313     * @see org.opencms.repository.I_CmsRepositorySession#getProperties(java.lang.String)
314     */
315    public Map<CmsPropertyName, String> getProperties(String path) throws CmsException {
316
317        Map<String, CmsProperty> props = m_cms.readProperties(path);
318        Map<CmsPropertyName, String> out = new HashMap<>();
319        for (Map.Entry<String, CmsProperty> entry : props.entrySet()) {
320            String name = entry.getKey();
321            CmsProperty prop = entry.getValue();
322            if (name.startsWith(EXTERNAL_PREFIX)) {
323                try {
324                    String remainder = name.substring(EXTERNAL_PREFIX.length());
325                    int pos = remainder.indexOf("_");
326                    String nsEncoded = remainder.substring(0, pos);
327                    String actualName = remainder.substring(pos + 1);
328                    String ns = decodeNamespace(nsEncoded);
329                    CmsPropertyName pn = new CmsPropertyName(ns, actualName);
330                    out.put(pn, prop.getStructureValue());
331                } catch (Exception e) {
332                    LOG.error(e.getLocalizedMessage(), e);
333                }
334            } else {
335                if (prop.getStructureValue() != null) {
336                    String outName = name + ".s";
337                    out.put(new CmsPropertyName(PROPERTY_NAMESPACE, outName), prop.getStructureValue());
338                }
339                if (prop.getResourceValue() != null) {
340                    String outName = name + ".r";
341                    out.put(new CmsPropertyName(PROPERTY_NAMESPACE, outName), prop.getResourceValue());
342                }
343            }
344        }
345        return out;
346    }
347
348    /**
349     * @see org.opencms.repository.I_CmsRepositorySession#list(java.lang.String)
350     */
351    public List<I_CmsRepositoryItem> list(String path) throws CmsException {
352
353        List<I_CmsRepositoryItem> ret = new ArrayList<I_CmsRepositoryItem>();
354
355        path = validatePath(path);
356
357        if (LOG.isDebugEnabled()) {
358            LOG.debug(Messages.get().getBundle().key(Messages.LOG_LIST_ITEMS_1, path));
359        }
360
361        List<CmsResource> resources = m_cms.getResourcesInFolder(path, CmsResourceFilter.DEFAULT);
362        Iterator<CmsResource> iter = resources.iterator();
363        while (iter.hasNext()) {
364            CmsResource res = iter.next();
365
366            if (!isFiltered(m_cms.getRequestContext().removeSiteRoot(res.getRootPath()))) {
367
368                // open the original resource (for virtual files this is the resource in the VFS
369                // which the virtual resource is based on)
370                // this filters e.g. property files for resources that are filtered out and thus
371                // should not be displayed
372                try {
373                    CmsResource org = m_cms.readResource(res.getStructureId(), CmsResourceFilter.DEFAULT);
374                    if (!isFiltered(m_cms.getRequestContext().removeSiteRoot(org.getRootPath()))) {
375                        ret.add(new CmsRepositoryItem(res, m_cms));
376                    }
377                } catch (CmsVfsResourceNotFoundException e) {
378                    // Pure virtual resources with an ID that does not correspond to any real resource
379                    ret.add(new CmsRepositoryItem(res, m_cms));
380                }
381            }
382        }
383
384        if (LOG.isDebugEnabled()) {
385            LOG.debug(Messages.get().getBundle().key(Messages.LOG_LIST_ITEMS_SUCESS_1, Integer.valueOf(ret.size())));
386        }
387
388        return ret;
389    }
390
391    /**
392     * @see org.opencms.repository.I_CmsRepositorySession#lock(java.lang.String, org.opencms.repository.CmsRepositoryLockInfo)
393     */
394    public boolean lock(String path, CmsRepositoryLockInfo lock) throws CmsException {
395
396        path = validatePath(path);
397
398        if (LOG.isDebugEnabled()) {
399            LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCK_ITEM_1, path));
400        }
401
402        m_cms.lockResource(path);
403        return true;
404    }
405
406    /**
407     * @see org.opencms.repository.I_CmsRepositorySession#move(java.lang.String, java.lang.String, boolean)
408     */
409    public void move(String src, String dest, boolean overwrite) throws CmsException {
410
411        src = validatePath(src);
412        dest = validatePath(dest);
413
414        if (LOG.isDebugEnabled()) {
415            LOG.debug(Messages.get().getBundle().key(Messages.LOG_MOVE_ITEM_2, src, dest));
416        }
417
418        // It is only possible in OpenCms to overwrite files.
419        // Folder are not possible to overwrite.
420        if (exists(dest)) {
421
422            if (overwrite) {
423                CmsResource srcRes = m_cms.readResource(src, CmsResourceFilter.DEFAULT);
424                CmsResource destRes = m_cms.readResource(dest, CmsResourceFilter.DEFAULT);
425
426                if ((srcRes.isFile()) && (destRes.isFile())) {
427
428                    if (LOG.isDebugEnabled()) {
429                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_DELETE_DEST_0));
430                    }
431
432                    // delete existing resource
433                    delete(dest);
434                } else {
435
436                    if (LOG.isDebugEnabled()) {
437                        LOG.debug(Messages.get().getBundle().key(Messages.ERR_OVERWRITE_0));
438                    }
439
440                    throw new CmsException(Messages.get().container(Messages.ERR_OVERWRITE_0));
441                }
442            } else {
443
444                if (LOG.isDebugEnabled()) {
445                    LOG.debug(Messages.get().getBundle().key(Messages.ERR_DEST_EXISTS_0));
446                }
447
448                throw new CmsVfsResourceAlreadyExistsException(Messages.get().container(Messages.ERR_DEST_EXISTS_0));
449            }
450        }
451
452        // lock source resource
453        m_cms.lockResource(src);
454
455        // moving
456        m_cms.moveResource(src, dest);
457
458        // unlock destination resource
459        m_cms.unlockResource(dest);
460    }
461
462    /**
463     * @see org.opencms.repository.I_CmsRepositorySession#save(java.lang.String, java.io.InputStream, boolean)
464     */
465    public void save(String path, InputStream inputStream, boolean overwrite) throws CmsException, IOException {
466
467        path = validatePath(path);
468        byte[] content = CmsFileUtil.readFully(inputStream);
469
470        try {
471            CmsFile file = m_cms.readFile(path, CmsResourceFilter.DEFAULT);
472
473            if (LOG.isDebugEnabled()) {
474                LOG.debug(Messages.get().getBundle().key(Messages.LOG_UPDATE_ITEM_1, path));
475            }
476
477            if (overwrite) {
478
479                file.setContents(content);
480
481                CmsLock lock = m_cms.getLock(file);
482
483                // lock resource
484                if (!lock.isInherited()) {
485                    m_cms.lockResource(path);
486                }
487
488                // write file
489                m_cms.writeFile(file);
490
491                if (lock.isNullLock()) {
492                    m_cms.unlockResource(path);
493                }
494            } else {
495
496                if (LOG.isDebugEnabled()) {
497                    LOG.debug(Messages.get().getBundle().key(Messages.ERR_DEST_EXISTS_0));
498                }
499
500                throw new CmsVfsResourceAlreadyExistsException(Messages.get().container(Messages.ERR_DEST_EXISTS_0));
501            }
502        } catch (CmsVfsResourceNotFoundException ex) {
503
504            if (LOG.isDebugEnabled()) {
505                LOG.debug(Messages.get().getBundle().key(Messages.LOG_CREATE_ITEM_1, path));
506            }
507
508            int type = OpenCms.getResourceManager().getDefaultTypeForName(path).getTypeId();
509
510            // create the file
511            CmsResource res = m_cms.createResource(path, type, content, null);
512
513            // unlock file after creation if lock is not inherited
514            if (!m_cms.getLock(res).isInherited()) {
515                m_cms.unlockResource(path);
516            }
517        }
518
519    }
520
521    /**
522     * @see org.opencms.repository.I_CmsRepositorySession#unlock(java.lang.String)
523     */
524    public void unlock(String path) {
525
526        try {
527            path = validatePath(path);
528
529            if (LOG.isDebugEnabled()) {
530                LOG.debug(Messages.get().getBundle().key(Messages.LOG_UNLOCK_ITEM_1, path));
531            }
532
533            m_cms.unlockResource(path);
534        } catch (CmsException ex) {
535
536            if (LOG.isErrorEnabled()) {
537                LOG.error(Messages.get().getBundle().key(Messages.ERR_UNLOCK_FAILED_0), ex);
538            }
539        }
540    }
541
542    /**
543     * @see org.opencms.repository.I_CmsRepositorySession#updateProperties(java.lang.String, java.util.Map)
544     */
545    public void updateProperties(String path, Map<CmsPropertyName, String> properties) throws CmsException {
546
547        Map<String, CmsProperty> propsToWrite = new HashMap<>();
548        for (Map.Entry<CmsPropertyName, String> entry : properties.entrySet()) {
549            CmsPropertyName pn = entry.getKey();
550            String value = entry.getValue();
551            if (pn.getNamespace().equals(PROPERTY_NAMESPACE)) {
552                String baseName = pn.getName().substring(0, pn.getName().length() - 2);
553                if (!propsToWrite.containsKey(baseName)) {
554                    CmsProperty prop = new CmsProperty(baseName, null, null);
555                    propsToWrite.put(baseName, prop);
556                }
557                CmsProperty prop = propsToWrite.get(baseName);
558                if (pn.getName().endsWith(".s")) {
559                    prop.setStructureValue(value);
560                } else if (pn.getName().endsWith(".r")) {
561                    prop.setResourceValue(value);
562                } else {
563                    LOG.error("Invalid name for repository property, must end with .s or .r");
564                }
565            } else {
566                try {
567                    String propName = EXTERNAL_PREFIX + encodeNamespace(pn.getNamespace()) + "_" + pn.getName();
568                    CmsProperty prop = new CmsProperty(propName, value, null);
569                    propsToWrite.put(propName, prop);
570                } catch (Exception e) {
571                    LOG.error(e.getLocalizedMessage(), e);
572                }
573            }
574        }
575        boolean needUnlock = false;
576        if (null == getLock(path)) {
577            m_cms.lockResource(path);
578            needUnlock = true;
579        }
580        try {
581            LOG.debug("Writing properties: " + propsToWrite);
582            m_cms.writeProperties(path, propsToWrite);
583        } finally {
584            if (needUnlock) {
585                m_cms.unlockResource(path);
586            }
587        }
588    }
589
590    /**
591     * Adds the site root to the path name and checks then if the path
592     * is filtered.<p>
593     *
594     * @see org.opencms.repository.A_CmsRepositorySession#isFiltered(java.lang.String)
595     */
596    @Override
597    protected boolean isFiltered(String name) {
598
599        boolean ret = super.isFiltered(m_cms.getRequestContext().addSiteRoot(name));
600        if (ret) {
601
602            if (LOG.isDebugEnabled()) {
603                LOG.debug(Messages.get().getBundle().key(Messages.ERR_ITEM_FILTERED_1, name));
604            }
605
606        }
607
608        return ret;
609    }
610
611    /**
612     * Gets the resource translator to use for path translations.
613     *
614     * @return the resource translator to use
615     */
616    private CmsResourceTranslator getEffectiveResourceTranslator() {
617
618        if (m_translation != null) {
619            return m_translation;
620        }
621        return m_cms.getRequestContext().getFileTranslator();
622    }
623
624    /**
625     * Validates (translates) the given path and checks if it is filtered out.<p>
626     *
627     * @param path the path to validate
628     *
629     * @return the validated path
630     *
631     * @throws CmsSecurityException if the path is filtered out
632     */
633    private String validatePath(String path) throws CmsSecurityException {
634
635        // Problems with spaces in new folders (default: "Neuer Ordner")
636        // Solution: translate this to a correct name.
637        CmsResourceTranslator translator = getEffectiveResourceTranslator();
638        String ret = CmsStringUtil.translatePathComponents(translator, path);
639
640        // add site root only works correct if system folder ends with a slash
641        if (CmsResource.VFS_FOLDER_SYSTEM.equals(ret)) {
642            ret = ret.concat("/");
643        }
644
645        // filter path
646        if (isFiltered(ret)) {
647            throw new CmsSecurityException(Messages.get().container(Messages.ERR_ITEM_FILTERED_1, path));
648        }
649
650        return ret;
651    }
652}