1<?php
2
3namespace Sabre\DAV\PropertyStorage\Backend;
4
5use Sabre\DAV\PropFind;
6use Sabre\DAV\PropPatch;
7use Sabre\DAV\Xml\Property\Complex;
8
9/**
10 * PropertyStorage PDO backend.
11 *
12 * This backend class uses a PDO-enabled database to store webdav properties.
13 * Both sqlite and mysql have been tested.
14 *
15 * The database structure can be found in the examples/sql/ directory.
16 *
17 * @copyright Copyright (C) 2007-2015 fruux GmbH. (https://fruux.com/)
18 * @author Evert Pot (http://evertpot.com/)
19 * @license http://sabre.io/license/ Modified BSD License
20 */
21class PDO implements BackendInterface {
22
23    /**
24     * Value is stored as string.
25     */
26    const VT_STRING = 1;
27
28    /**
29     * Value is stored as XML fragment.
30     */
31    const VT_XML = 2;
32
33    /**
34     * Value is stored as a property object.
35     */
36    const VT_OBJECT = 3;
37
38    /**
39     * PDO
40     *
41     * @var \PDO
42     */
43    protected $pdo;
44
45    /**
46     * Creates the PDO property storage engine
47     *
48     * @param \PDO $pdo
49     */
50    function __construct(\PDO $pdo) {
51
52        $this->pdo = $pdo;
53
54    }
55
56    /**
57     * Fetches properties for a path.
58     *
59     * This method received a PropFind object, which contains all the
60     * information about the properties that need to be fetched.
61     *
62     * Ususually you would just want to call 'get404Properties' on this object,
63     * as this will give you the _exact_ list of properties that need to be
64     * fetched, and haven't yet.
65     *
66     * However, you can also support the 'allprops' property here. In that
67     * case, you should check for $propFind->isAllProps().
68     *
69     * @param string $path
70     * @param PropFind $propFind
71     * @return void
72     */
73    function propFind($path, PropFind $propFind) {
74
75        if (!$propFind->isAllProps() && count($propFind->get404Properties()) === 0) {
76            return;
77        }
78
79        $query = 'SELECT name, value, valuetype FROM propertystorage WHERE path = ?';
80        $stmt = $this->pdo->prepare($query);
81        $stmt->execute([$path]);
82
83        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
84            switch ($row['valuetype']) {
85                case null :
86                case self::VT_STRING :
87                    $propFind->set($row['name'], $row['value']);
88                    break;
89                case self::VT_XML :
90                    $propFind->set($row['name'], new Complex($row['value']));
91                    break;
92                case self::VT_OBJECT :
93                    $propFind->set($row['name'], unserialize($row['value']));
94                    break;
95            }
96        }
97
98    }
99
100    /**
101     * Updates properties for a path
102     *
103     * This method received a PropPatch object, which contains all the
104     * information about the update.
105     *
106     * Usually you would want to call 'handleRemaining' on this object, to get;
107     * a list of all properties that need to be stored.
108     *
109     * @param string $path
110     * @param PropPatch $propPatch
111     * @return void
112     */
113    function propPatch($path, PropPatch $propPatch) {
114
115        $propPatch->handleRemaining(function($properties) use ($path) {
116
117            $updateStmt = $this->pdo->prepare("REPLACE INTO propertystorage (path, name, valuetype, value) VALUES (?, ?, ?, ?)");
118            $deleteStmt = $this->pdo->prepare("DELETE FROM propertystorage WHERE path = ? AND name = ?");
119
120            foreach ($properties as $name => $value) {
121
122                if (!is_null($value)) {
123                    if (is_scalar($value)) {
124                        $valueType = self::VT_STRING;
125                    } elseif ($value instanceof Complex) {
126                        $valueType = self::VT_XML;
127                        $value = $value->getXml();
128                    } else {
129                        $valueType = self::VT_OBJECT;
130                        $value = serialize($value);
131                    }
132                    $updateStmt->execute([$path, $name, $valueType, $value]);
133                } else {
134                    $deleteStmt->execute([$path, $name]);
135                }
136
137            }
138
139            return true;
140
141        });
142
143    }
144
145    /**
146     * This method is called after a node is deleted.
147     *
148     * This allows a backend to clean up all associated properties.
149     *
150     * The delete method will get called once for the deletion of an entire
151     * tree.
152     *
153     * @param string $path
154     * @return void
155     */
156    function delete($path) {
157
158        $stmt = $this->pdo->prepare("DELETE FROM propertystorage WHERE path = ? OR path LIKE ? ESCAPE '='");
159        $childPath = strtr(
160            $path,
161            [
162                '=' => '==',
163                '%' => '=%',
164                '_' => '=_'
165            ]
166        ) . '/%';
167
168        $stmt->execute([$path, $childPath]);
169
170    }
171
172    /**
173     * This method is called after a successful MOVE
174     *
175     * This should be used to migrate all properties from one path to another.
176     * Note that entire collections may be moved, so ensure that all properties
177     * for children are also moved along.
178     *
179     * @param string $source
180     * @param string $destination
181     * @return void
182     */
183    function move($source, $destination) {
184
185        // I don't know a way to write this all in a single sql query that's
186        // also compatible across db engines, so we're letting PHP do all the
187        // updates. Much slower, but it should still be pretty fast in most
188        // cases.
189        $select = $this->pdo->prepare('SELECT id, path FROM propertystorage WHERE path = ? OR path LIKE ?');
190        $select->execute([$source, $source . '/%']);
191
192        $update = $this->pdo->prepare('UPDATE propertystorage SET path = ? WHERE id = ?');
193        while ($row = $select->fetch(\PDO::FETCH_ASSOC)) {
194
195            // Sanity check. SQL may select too many records, such as records
196            // with different cases.
197            if ($row['path'] !== $source && strpos($row['path'], $source . '/') !== 0) continue;
198
199            $trailingPart = substr($row['path'], strlen($source) + 1);
200            $newPath = $destination;
201            if ($trailingPart) {
202                $newPath .= '/' . $trailingPart;
203            }
204            $update->execute([$newPath, $row['id']]);
205
206        }
207
208    }
209
210}
211