owncloud/core

View on GitHub
lib/private/Lock/Persistent/LockMapper.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php
/**
 * @author Thomas Müller <thomas.mueller@tmit.eu>
 * @copyright Copyright (c) 2018, ownCloud GmbH
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */

namespace OC\Lock\Persistent;

use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\Mapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IDBConnection;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Lock\Persistent\ILock;

class LockMapper extends Mapper {
    /** @var ITimeFactory */
    private $timeFactory;

    public function __construct(IDBConnection $db, ITimeFactory $timeFactory) {
        parent::__construct($db, 'persistent_locks', null);
        $this->timeFactory = $timeFactory;
    }

    /**
     * Returns an array of all paths to each of
     * the parents from the given path.
     *
     * Ex: if "/a/b/c" is given, returns ["/a", "/a/b"]
     *
     * @param string $path
     * @return string[] array of parent paths
     */
    private function getParentPaths($path) {
        // We need to check locks for every part in the uri.
        $uriParts = \explode('/', $path);

        // only return parents, not the current one
        \array_pop($uriParts);

        $parentPaths = [];

        $currentPath = '';
        foreach ($uriParts as $part) {
            if ($currentPath) {
                $currentPath .= '/';
            }
            $currentPath .= $part;
            $parentPaths[] = $currentPath;
        }

        return $parentPaths;
    }

    /**
     * Selects all locks from the database for the given storage and path.
     * Locks for parent folders are returned as well as long as they have a non-zero depth,
     * as these would mean the target $internalPath is indirectly locked through these.
     * In case $returnChildLocks is true, locks for all children paths will be return as well,
     * regardless of depth..
     *
     * Examples:
     *
     * When function called with "foo/bar" the returned array will have lock
     * entries for the following, given the conditions are met:
     * - "foo" if the latter has a lock set with non-zero Depth
     * - "foo/bar" if the latter has a lock set (current target)
     * - "foo/bar/sub" if the latter has a lock set, and given $returnChildLocks is true
     *
     * @param int $storageId numeric id of the storage for which to retrieve lock entries
     * @param string $internalPath target internal path
     * @param bool $returnChildLocks whether to return any lock for any child
     * @return Lock[]
     */
    public function getLocksByPath($storageId, $internalPath, $returnChildLocks) {
        $query = $this->db->getQueryBuilder();
        $internalPath = \rtrim($internalPath, '/');

        /*
         * SELECT `id`, `owner`, `timeout`, `created_at`, `token`, `token`, `scope`, `depth`, `file_id`, `path`, `owner_account_id`
         * FROM `oc_persistent_locks` l
         * INNER JOIN `oc_filecache` f ON l.`file_id` = f.`fileid`
         * WHERE (
         *     (`storage` = 4)
         *     AND (`created_at` > (1544710587 - `timeout`))
         *     AND (
         *         (f.`path` = 'files/test/target')
         *         OR ((`depth` <> 0) AND (`path` in ('files', 'files/test')))
         *     )
         * );
         */
        $query->select(['id', 'owner', 'timeout', 'created_at', 'token', 'token', 'scope', 'depth', 'file_id', 'path', 'owner_account_id'])
            ->from($this->getTableName(), 'l')
            ->join('l', 'filecache', 'f', $query->expr()->eq('l.file_id', 'f.fileid'))
            // WHERE (`storage` = 4)
            ->where($query->expr()->eq('storage', $query->createPositionalParameter($storageId)))
            // AND (`created_at` > (1544710587 - `timeout`))
            ->andWhere($query->expr()->gt('created_at', $query->createFunction('(' . $query->createPositionalParameter($this->timeFactory->getTime()) . ' - `timeout`)')));

        $pathMatchClauses = $query->expr()->orX(
            // direct match
            // (f.`path` = 'files/test/target')
            $query->expr()->eq('f.path', $query->createPositionalParameter($internalPath))
        );

        if ($returnChildLocks) {
            $pathMatchClauses->add(
                // match all children paths from the current path
                // (f.`path` LIKE 'files/test/target/%')
                $query->expr()->like('f.path', $query->createPositionalParameter($this->db->escapeLikeParameter($internalPath) . '/%'))
            );
        }

        $parentPaths = $this->getParentPaths($internalPath);
        if (!empty($parentPaths)) {
            // match any parents with the condition that there is a lock with non-zero Depth
            $pathMatchClauses->add(
                $query->expr()->andX(
                    $query->expr()->neq('depth', $query->createPositionalParameter(0)),
                    // here we are assuming that the number of path sections will be less than 1000
                    $query->expr()->in('path', $query->createPositionalParameter($parentPaths, IQueryBuilder::PARAM_STR_ARRAY))
                )
            );
        }

        $query->andWhere($pathMatchClauses);

        $stmt = $query->execute();
        $entities = [];
        while ($row = $stmt->fetch()) {
            $entities[] = $this->mapRowToEntity($row);
        }
        $stmt->closeCursor();

        return $entities;
    }

    /**
     * @param int $fileId
     * @param string $token
     * @return bool
     */
    public function deleteByFileIdAndToken($fileId, $token) {
        $query = $this->db->getQueryBuilder();

        $rowCount = $query->delete($this->getTableName())
            ->where($query->expr()->eq('file_id', $query->createNamedParameter($fileId)))
            ->andWhere($query->expr()->eq('token_hash', $query->createNamedParameter(\md5($token))))
            ->execute();

        return $rowCount === 1;
    }

    /**
     * @param string $token
     * @return Lock
     * @throws \OCP\AppFramework\Db\DoesNotExistException
     * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
     */
    public function getLockByToken($token) {
        $query = $this->db->getQueryBuilder();

        $query->select(['id', 'owner', 'timeout', 'created_at', 'token', 'token_hash', 'scope', 'depth', 'file_id', 'owner_account_id'])
            ->from($this->getTableName(), 'l')
            ->where($query->expr()->eq('token_hash', $query->createNamedParameter(\md5($token))));

        return $this->findEntity($query->getSQL(), $query->getParameters());
    }

    private function validateEntity($entity) {
        if (!$entity instanceof Lock) {
            throw new \InvalidArgumentException('Wrong entity type used');
        }
        if (\md5($entity->getToken()) !== $entity->getTokenHash()) {
            throw new \InvalidArgumentException('token_hash does not match the token of the lock');
        }
        if ($entity->getDepth() !== ILock::LOCK_DEPTH_ZERO && $entity->getDepth() !== ILock::LOCK_DEPTH_INFINITE) {
            throw new \InvalidArgumentException('Only -1 (infinity) and 0 are supported for lock depth, ' . $entity->getDepth() . ' given');
        }
    }

    public function insert(Entity $entity) {
        $this->validateEntity($entity);
        return parent::insert($entity);
    }

    public function update(Entity $entity) {
        $this->validateEntity($entity);
        return parent::update($entity);
    }

    public function cleanup() {
        $query = $this->db->getQueryBuilder();
        $query->delete($this->getTableName())
            ->where($query->expr()->lt(
                'created_at',
                $query->createFunction('(' . $query->createNamedParameter($this->timeFactory->getTime()) . ' - `timeout`)')
            ))
            ->execute();
    }
}