Completed
Push — master ( 4f8b81...65d286 )
by
unknown
05:00
created

ItemMapper   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 388
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 88.53%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 38
c 1
b 0
f 0
lcom 1
cbo 2
dl 0
loc 388
ccs 193
cts 218
cp 0.8853
rs 8.3999

26 Methods

Rating   Name   Duplication   Size   Complexity  
A buildLikeParameters() 0 6 1
A __construct() 0 3 1
A starredCount() 0 19 1
A readAll() 0 12 1
A readFolder() 0 14 1
A getOperator() 0 7 2
A findAllNew() 0 6 1
A findAllNewFolder() 0 7 1
A findAllNewFeed() 0 7 1
A findEntitiesIgnoringNegativeLimit() 0 8 2
A findAllFeed() 0 16 2
A findAllFolder() 0 16 2
A findAll() 0 15 2
A findAllUnreadOrStarred() 0 7 1
A findByGuidHash() 0 7 1
B deleteReadOlderThanThreshold() 0 37 3
A getNewestItemId() 0 12 1
A deleteUser() 0 9 1
A makeSelectQuery() 0 20 2
A makeSelectQueryStatus() 0 13 1
A readFeed() 0 15 1
A find() 0 4 1
A updateSearchIndices() 0 14 2
A findAllIds() 0 4 1
A updateSearchIndex() 0 13 3
B readItem() 0 24 2
1
<?php
2
/**
3
 * ownCloud - News
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Alessandro Cosentino <[email protected]>
9
 * @author Bernhard Posselt <[email protected]>
10
 * @copyright Alessandro Cosentino 2012
11
 * @copyright Bernhard Posselt 2012, 2014
12
 */
13
14
namespace OCA\News\Db;
15
16
use Exception;
17
use OCP\IDBConnection;
18
19
20
class ItemMapper extends NewsMapper {
21
22 35
    public function __construct(IDBConnection $db){
23 35
        parent::__construct($db, 'news_items', Item::class);
24 35
    }
25
26
27 21
    private function makeSelectQuery($prependTo='', $oldestFirst=false,
28
                                     $distinctFingerprint=false){
29 21
        if($oldestFirst) {
30 3
            $ordering = 'ASC';
31 3
        } else {
32 18
            $ordering = 'DESC';
33
        }
34
35
        return 'SELECT `items`.* FROM `*PREFIX*news_items` `items` '.
36 21
            'JOIN `*PREFIX*news_feeds` `feeds` ' .
37 21
                'ON `feeds`.`id` = `items`.`feed_id` '.
38 21
                'AND `feeds`.`deleted_at` = 0 ' .
39 21
                'AND `feeds`.`user_id` = ? ' .
40 21
                $prependTo .
41 21
            'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' .
42 21
                'ON `folders`.`id` = `feeds`.`folder_id` ' .
43 21
            'WHERE `feeds`.`folder_id` = 0 ' .
44 21
                'OR `folders`.`deleted_at` = 0 ' .
45 21
            'ORDER BY `items`.`id` ' . $ordering;
46
    }
47
48 18
    private function makeSelectQueryStatus($prependTo, $status,
49
                                           $oldestFirst=false, $search=[],
50
                                           $distinctFingerprint=false) {
51 18
        $status = (int) $status;
52 18
        $count = count($search);
53
54
        // WARNING: Potential SQL injection if you change this carelessly
55 18
        $sql = 'AND ((`items`.`status` & ' . $status . ') = ' . $status . ') ';
56 18
        $sql .= str_repeat('AND `items`.`search_index` LIKE ? ', $count);
57 18
        $sql .= $prependTo;
58
59 18
        return $this->makeSelectQuery($sql, $oldestFirst, $distinctFingerprint);
60
    }
61
62
	/**
63
	 * wrap and escape search parameters in a like statement
64
	 *
65
	 * @param string[] $search an array of strings that should be searched
66
	 * @return array with like parameters
67
	 */
68
    private function buildLikeParameters($search=[]) {
69 15
        return array_map(function ($param) {
70 3
            $param = addcslashes($param, '\\_%');
71 3
            return '%' . mb_strtolower($param, 'UTF-8') . '%';
72 15
        }, $search);
73
    }
74
75
    /**
76
     * @param int $id
77
     * @param string $userId
78
     * @return \OCA\News\Db\Item
79
     */
80 1
    public function find($id, $userId){
81 1
        $sql = $this->makeSelectQuery('AND `items`.`id` = ? ');
82 1
        return $this->findEntity($sql, [$userId, $id]);
83
    }
84
85 1
    public function starredCount($userId){
86
        $sql = 'SELECT COUNT(*) AS size FROM `*PREFIX*news_items` `items` '.
87 1
            'JOIN `*PREFIX*news_feeds` `feeds` ' .
88 1
                'ON `feeds`.`id` = `items`.`feed_id` '.
89 1
                'AND `feeds`.`deleted_at` = 0 ' .
90 1
                'AND `feeds`.`user_id` = ? ' .
91 1
                'AND ((`items`.`status` & ' . StatusFlag::STARRED . ') = ' .
92 1
                StatusFlag::STARRED . ')' .
93 1
            'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' .
94 1
                'ON `folders`.`id` = `feeds`.`folder_id` ' .
95 1
            'WHERE `feeds`.`folder_id` = 0 ' .
96 1
                'OR `folders`.`deleted_at` = 0';
97
98 1
        $params = [$userId];
99
100 1
        $result = $this->execute($sql, $params)->fetch();
101
102 1
        return (int) $result['size'];
103
    }
104
105
106 1
    public function readAll($highestItemId, $time, $userId) {
107
        $sql = 'UPDATE `*PREFIX*news_items` ' .
108 1
            'SET `status` = `status` & ? ' .
109 1
            ', `last_modified` = ? ' .
110 1
            'WHERE `feed_id` IN (' .
111 1
                'SELECT `id` FROM `*PREFIX*news_feeds` ' .
112 1
                    'WHERE `user_id` = ? ' .
113 1
                ') '.
114 1
            'AND `id` <= ?';
115 1
        $params = [~StatusFlag::UNREAD, $time, $userId, $highestItemId];
116 1
        $this->execute($sql, $params);
117 1
    }
118
119
120 1
    public function readFolder($folderId, $highestItemId, $time, $userId) {
121
        $sql = 'UPDATE `*PREFIX*news_items` ' .
122 1
            'SET `status` = `status` & ? ' .
123 1
            ', `last_modified` = ? ' .
124 1
            'WHERE `feed_id` IN (' .
125 1
                'SELECT `id` FROM `*PREFIX*news_feeds` ' .
126 1
                    'WHERE `folder_id` = ? ' .
127 1
                    'AND `user_id` = ? ' .
128 1
                ') '.
129 1
            'AND `id` <= ?';
130 1
        $params = [~StatusFlag::UNREAD, $time, $folderId, $userId,
131 1
            $highestItemId];
132 1
        $this->execute($sql, $params);
133 1
    }
134
135
136 1
    public function readFeed($feedId, $highestItemId, $time, $userId){
137
        $sql = 'UPDATE `*PREFIX*news_items` ' .
138 1
            'SET `status` = `status` & ? ' .
139 1
            ', `last_modified` = ? ' .
140 1
                'WHERE `feed_id` = ? ' .
141 1
                'AND `id` <= ? ' .
142 1
                'AND EXISTS (' .
143 1
                    'SELECT * FROM `*PREFIX*news_feeds` ' .
144 1
                    'WHERE `user_id` = ? ' .
145 1
                    'AND `id` = ? ) ';
146 1
        $params = [~StatusFlag::UNREAD, $time, $feedId, $highestItemId,
147 1
            $userId, $feedId];
148
149 1
        $this->execute($sql, $params);
150 1
    }
151
152
153 12
    private function getOperator($oldestFirst) {
154 12
        if($oldestFirst) {
155 3
            return '>';
156
        } else {
157 9
            return '<';
158
        }
159
    }
160
161
162 1
    public function findAllNew($updatedSince, $status, $userId){
163 1
        $sql = $this->makeSelectQueryStatus(
164 1
            'AND `items`.`last_modified` >= ? ', $status);
165 1
        $params = [$userId, $updatedSince];
166 1
        return $this->findEntities($sql, $params);
167
    }
168
169
170 1
    public function findAllNewFolder($id, $updatedSince, $status, $userId){
171
        $sql = 'AND `feeds`.`folder_id` = ? ' .
172 1
                'AND `items`.`last_modified` >= ? ';
173 1
        $sql = $this->makeSelectQueryStatus($sql, $status);
174 1
        $params = [$userId, $id, $updatedSince];
175 1
        return $this->findEntities($sql, $params);
176
    }
177
178
179 1
    public function findAllNewFeed($id, $updatedSince, $status, $userId){
180
        $sql = 'AND `items`.`feed_id` = ? ' .
181 1
                'AND `items`.`last_modified` >= ? ';
182 1
        $sql = $this->makeSelectQueryStatus($sql, $status);
183 1
        $params = [$userId, $id, $updatedSince];
184 1
        return $this->findEntities($sql, $params);
185
    }
186
187
188 15
    private function findEntitiesIgnoringNegativeLimit($sql, $params, $limit) {
189
        // ignore limit if negative to offer a way to return all feeds
190 15
        if ($limit >= 0) {
191 12
            return $this->findEntities($sql, $params, $limit);
192
        } else {
193 3
            return $this->findEntities($sql, $params);
194
        }
195
    }
196
197
198 5
    public function findAllFeed($id, $limit, $offset, $status, $oldestFirst,
199
                                $userId, $search=[]){
200 5
        $params = [$userId];
201 5
        $params = array_merge($params, $this->buildLikeParameters($search));
202 5
        $params[] = $id;
203
204 5
        $sql = 'AND `items`.`feed_id` = ? ';
205 5
        if($offset !== 0){
206
            $sql .= 'AND `items`.`id` ' .
207 4
                $this->getOperator($oldestFirst) . ' ? ';
208 4
            $params[] = $offset;
209 4
        }
210 5
        $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst,
211 5
                                            $search);
212 5
        return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
213
    }
214
215
216 5
    public function findAllFolder($id, $limit, $offset, $status, $oldestFirst,
217
                                  $userId, $search=[]){
218 5
        $params = [$userId];
219 5
        $params = array_merge($params, $this->buildLikeParameters($search));
220 5
        $params[] = $id;
221
222 5
        $sql = 'AND `feeds`.`folder_id` = ? ';
223 5
        if($offset !== 0){
224
            $sql .= 'AND `items`.`id` ' .
225 4
                $this->getOperator($oldestFirst) . ' ? ';
226 4
            $params[] = $offset;
227 4
        }
228 5
        $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst,
229 5
                                            $search);
230 5
        return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
231
    }
232
233
234 5
    public function findAll($limit, $offset, $status, $oldestFirst, $userId,
235
                            $search=[]){
236 5
        $params = [$userId];
237 5
        $params = array_merge($params, $this->buildLikeParameters($search));
238 5
        $sql = '';
239 5
        if($offset !== 0){
240
            $sql .= 'AND `items`.`id` ' .
241 4
                $this->getOperator($oldestFirst) . ' ? ';
242 4
            $params[] = $offset;
243 4
        }
244 5
        $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst,
245 5
                                            $search);
246
247 5
        return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
248
    }
249
250
251 1
    public function findAllUnreadOrStarred($userId) {
252 1
        $params = [$userId];
253 1
        $status = StatusFlag::UNREAD | StatusFlag::STARRED;
254 1
        $sql = 'AND ((`items`.`status` & ' . $status . ') > 0) ';
255 1
        $sql = $this->makeSelectQuery($sql);
256 1
        return $this->findEntities($sql, $params);
257
    }
258
259
260 1
    public function findByGuidHash($guidHash, $feedId, $userId){
261 1
        $sql = $this->makeSelectQuery(
262
            'AND `items`.`guid_hash` = ? ' .
263 1
            'AND `feeds`.`id` = ? ');
264
265 1
        return $this->findEntity($sql, [$userId, $guidHash, $feedId]);
266
    }
267
268
269
    /**
270
     * Delete all items for feeds that have over $threshold unread and not
271
     * starred items
272
	 * @param int $threshold the number of items that should be deleted
273
     */
274 2
    public function deleteReadOlderThanThreshold($threshold){
275 2
        $status = StatusFlag::STARRED | StatusFlag::UNREAD;
276 2
        $params = [$status, $threshold];
277
278
        $sql = 'SELECT (COUNT(*) - `feeds`.`articles_per_update`) AS `size`, ' .
279 2
        '`feeds`.`id` AS `feed_id`, `feeds`.`articles_per_update` ' .
280 2
            'FROM `*PREFIX*news_items` `items` ' .
281 2
            'JOIN `*PREFIX*news_feeds` `feeds` ' .
282 2
                'ON `feeds`.`id` = `items`.`feed_id` ' .
283 2
                'AND NOT ((`items`.`status` & ?) > 0) ' .
284 2
            'GROUP BY `feeds`.`id`, `feeds`.`articles_per_update` ' .
285 2
            'HAVING COUNT(*) > ?';
286
287 2
        $result = $this->execute($sql, $params);
288
289 2
        while($row = $result->fetch()) {
290
291 2
            $size = (int) $row['size'];
292 2
            $limit = $size - $threshold;
293
294 2
            if($limit > 0) {
295 1
                $params = [$status, $row['feed_id'], $limit];
296
297
                $sql = 'DELETE FROM `*PREFIX*news_items` ' .
298 1
                'WHERE `id` IN (' .
299 1
                    'SELECT `id` FROM `*PREFIX*news_items` ' .
300 1
                    'WHERE NOT ((`status` & ?) > 0) ' .
301 1
                    'AND `feed_id` = ? ' .
302 1
                    'ORDER BY `id` ASC ' .
303 1
                    'LIMIT ?' .
304 1
                ')';
305
306 1
                $this->execute($sql, $params);
307 1
            }
308 2
        }
309
310 2
    }
311
312
313 2
    public function getNewestItemId($userId) {
314
        $sql = 'SELECT MAX(`items`.`id`) AS `max_id` ' .
315 2
            'FROM `*PREFIX*news_items` `items` '.
316 2
            'JOIN `*PREFIX*news_feeds` `feeds` ' .
317 2
                'ON `feeds`.`id` = `items`.`feed_id` '.
318 2
                'AND `feeds`.`user_id` = ?';
319 2
        $params = [$userId];
320
321 2
        $result = $this->findOneQuery($sql, $params);
322
323 1
        return (int) $result['max_id'];
324
    }
325
326
327
    /**
328
     * Deletes all items of a user
329
     * @param string $userId the name of the user
330
     */
331 1
    public function deleteUser($userId) {
332
        $sql = 'DELETE FROM `*PREFIX*news_items` ' .
333 1
            'WHERE `feed_id` IN (' .
334 1
                'SELECT `feeds`.`id` FROM `*PREFIX*news_feeds` `feeds` ' .
335 1
                    'WHERE `feeds`.`user_id` = ?' .
336 1
                ')';
337
338 1
        $this->execute($sql, [$userId]);
339 1
    }
340
341
342
    /**
343
     * Returns a list of ids and userid of all items
344
     */
345
    public function findAllIds($limit=null, $offset=null) {
346
        $sql = 'SELECT `id` FROM `*PREFIX*news_items`';
347
        return $this->execute($sql, [], $limit, $offset)->fetchAll();
348
    }
349
350
    /**
351
     * Update search indices of all items
352
     */
353
    public function updateSearchIndices() {
354
        // update indices in steps to prevent memory issues on larger systems
355
        $step = 1000;  // update 1000 items at a time
356
        $itemCount = 1;
357
        $offset = 0;
358
359
        // stop condition if there are no previously fetched items
360
        while ($itemCount > 0) {
361
            $items = $this->findAllIds($step, $offset);
362
            $itemCount = count($items);
363
            $this->updateSearchIndex($items);
364
            $offset += $step;
365
        }
366
    }
367
368
    private function updateSearchIndex(array $items=[]) {
369
        foreach ($items as $row) {
370
            try {
371
                $sql = 'SELECT * FROM `*PREFIX*news_items` WHERE `id` = ?';
372
                $params = [$row['id']];
373
                $item = $this->findEntity($sql, $params);
374
                $item->generateSearchIndex();
375
                $this->update($item);
376
            } catch (Exception $e) {
377
                continue;
378
            }
379
        }
380
    }
381
382
    public function readItem($itemId, $isRead, $lastModified, $userId) {
383
        $item = $this->find($itemId, $userId);
384
385
        // reading an item should set all of the same items as read, whereas
386
        // marking an item as unread should only mark the selected instance
387
        // as unread
388
        if ($isRead) {
389
            $sql = 'UPDATE `*PREFIX*news_items`
390
                SET `status` = `status` & ?
391
                    AND `last_modified` = ?
392
                WHERE `fingerprint` = ?
393
                    AND feed_id IN (
394
                        SELECT `f`.`id` FROM `*PREFIX*news_feeds` `f`
395
                            WHERE `f`.`user_id` = ?
396
                    )';
397
            $params = [~StatusFlag::UNREAD, $lastModified,
398
                       $item->getFingerprint(), $userId];
399
            $this->execute($sql, $params);
400
        } else {
401
            $item->setLastModified($lastModified);
402
            $item->setUnread();
403
            $this->update($item);
404
        }
405
    }
406
407
}
408