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