Completed
Push — master ( 469563...250922 )
by
unknown
10:02
created

ItemMapper   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 367
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 97.47%

Importance

Changes 7
Bugs 0 Features 1
Metric Value
wmc 36
c 7
b 0
f 1
lcom 1
cbo 2
dl 0
loc 367
ccs 193
cts 198
cp 0.9747
rs 8.8

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A makeSelectQuery() 0 20 2
A makeSelectQueryStatus() 0 13 1
A buildLikeParameters() 0 6 1
A find() 0 4 1
A starredCount() 0 19 1
A readAll() 0 12 1
A readFolder() 0 14 1
A readFeed() 0 15 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 findAllItemIdsAndUsers() 0 7 1
A updateSearchIndices() 0 14 2
A updateSearchIndex() 0 13 3
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 OCP\AppFramework\Db\DoesNotExistException;
17
use OCP\IDBConnection;
18
19
20
class ItemMapper extends NewsMapper {
21 35
22 35
    public function __construct(IDBConnection $db){
23 35
        parent::__construct($db, 'news_items', Item::class);
24
    }
25
26 21
27
    private function makeSelectQuery($prependTo='', $oldestFirst=false,
28 21
                                     $distinctFingerprint=false){
29 3
        if($oldestFirst) {
30 3
            $ordering = 'ASC';
31 18
        } else {
32
            $ordering = 'DESC';
33
        }
34
35 21
        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
            'ORDER BY `items`.`id` ' . $ordering;
46
    }
47 18
48
    private function makeSelectQueryStatus($prependTo, $status,
49
                                           $oldestFirst=false, $search=[],
50 18
                                           $distinctFingerprint=false) {
51 18
        $status = (int) $status;
52
        $count = count($search);
53
54 18
        // 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
        $sql .= $prependTo;
58 18
59
        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 15
    private function buildLikeParameters($search=[]) {
69 3
        return array_map(function ($param) {
70 3
            $param = addcslashes($param, '\\_%');
71 15
            return '%' . strtolower($param) . '%';
72
        }, $search);
73
    }
74 1
75 1
    /**
76 1
     * @param int $id
77
     * @param string $userId
78
     * @return \OCA\News\Db\Item
79
     */
80 1
    public function find($id, $userId){
81
        $sql = $this->makeSelectQuery('AND `items`.`id` = ? ');
82 1
        return $this->findEntity($sql, [$userId, $id]);
83 1
    }
84 1
85 1
    public function starredCount($userId){
86 1
        $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
                StatusFlag::STARRED . ')' .
93 1
            'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' .
94
                'ON `folders`.`id` = `feeds`.`folder_id` ' .
95 1
            'WHERE `feeds`.`folder_id` = 0 ' .
96
                'OR `folders`.`deleted_at` = 0';
97 1
98
        $params = [$userId];
99
100
        $result = $this->execute($sql, $params)->fetch();
101 1
102
        return (int) $result['size'];
103 1
    }
104 1
105 1
106 1
    public function readAll($highestItemId, $time, $userId) {
107 1
        $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
                ') '.
114
            'AND `id` <= ?';
115 1
        $params = [~StatusFlag::UNREAD, $time, $userId, $highestItemId];
116
        $this->execute($sql, $params);
117 1
    }
118 1
119 1
120 1
    public function readFolder($folderId, $highestItemId, $time, $userId) {
121 1
        $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
            'AND `id` <= ?';
130
        $params = [~StatusFlag::UNREAD, $time, $folderId, $userId,
131 1
            $highestItemId];
132
        $this->execute($sql, $params);
133 1
    }
134 1
135 1
136 1
    public function readFeed($feedId, $highestItemId, $time, $userId){
137 1
        $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
                    'SELECT * FROM `*PREFIX*news_feeds` ' .
144 1
                    'WHERE `user_id` = ? ' .
145 1
                    'AND `id` = ? ) ';
146
        $params = [~StatusFlag::UNREAD, $time, $feedId, $highestItemId,
147
            $userId, $feedId];
148 12
149 12
        $this->execute($sql, $params);
150 3
    }
151
152 9
153
    private function getOperator($oldestFirst) {
154
        if($oldestFirst) {
155
            return '>';
156
        } else {
157 1
            return '<';
158 1
        }
159 1
    }
160 1
161 1
162
    public function findAllNew($updatedSince, $status, $userId){
163
        $sql = $this->makeSelectQueryStatus(
164
            'AND `items`.`last_modified` >= ? ', $status);
165 1
        $params = [$userId, $updatedSince];
166
        return $this->findEntities($sql, $params);
167 1
    }
168 1
169 1
170 1
    public function findAllNewFolder($id, $updatedSince, $status, $userId){
171
        $sql = 'AND `feeds`.`folder_id` = ? ' .
172
                'AND `items`.`last_modified` >= ? ';
173
        $sql = $this->makeSelectQueryStatus($sql, $status);
174 1
        $params = [$userId, $id, $updatedSince];
175
        return $this->findEntities($sql, $params);
176 1
    }
177 1
178 1
179 1
    public function findAllNewFeed($id, $updatedSince, $status, $userId){
180
        $sql = 'AND `items`.`feed_id` = ? ' .
181
                'AND `items`.`last_modified` >= ? ';
182
        $sql = $this->makeSelectQueryStatus($sql, $status);
183 15
        $params = [$userId, $id, $updatedSince];
184
        return $this->findEntities($sql, $params);
185 15
    }
186 12
187
188 3
    private function findEntitiesIgnoringNegativeLimit($sql, $params, $limit) {
189
        // ignore limit if negative to offer a way to return all feeds
190
        if ($limit >= 0) {
191
            return $this->findEntities($sql, $params, $limit);
192
        } else {
193 5
            return $this->findEntities($sql, $params);
194
        }
195 5
    }
196 5
197 5
198
    public function findAllFeed($id, $limit, $offset, $status, $oldestFirst,
199 5
                                $userId, $search=[]){
200 5
        $params = [$userId];
201
        $params = array_merge($params, $this->buildLikeParameters($search));
202 4
        $params[] = $id;
203 4
204 4
        $sql = 'AND `items`.`feed_id` = ? ';
205 5
        if($offset !== 0){
206 5
            $sql .= 'AND `items`.`id` ' .
207 5
                $this->getOperator($oldestFirst) . ' ? ';
208
            $params[] = $offset;
209
        }
210
        $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst,
211 5
                                            $search);
212
        return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
213 5
    }
214 5
215 5
216
    public function findAllFolder($id, $limit, $offset, $status, $oldestFirst,
217 5
                                  $userId, $search=[]){
218 5
        $params = [$userId];
219
        $params = array_merge($params, $this->buildLikeParameters($search));
220 4
        $params[] = $id;
221 4
222 4
        $sql = 'AND `feeds`.`folder_id` = ? ';
223 5
        if($offset !== 0){
224 5
            $sql .= 'AND `items`.`id` ' .
225 5
                $this->getOperator($oldestFirst) . ' ? ';
226
            $params[] = $offset;
227
        }
228
        $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst,
229 5
                                            $search);
230
        return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
231 5
    }
232 5
233 5
234 5
    public function findAll($limit, $offset, $status, $oldestFirst, $userId,
235
                            $search=[]){
236 4
        $params = [$userId];
237 4
        $params = array_merge($params, $this->buildLikeParameters($search));
238 4
        $sql = '';
239 5
        if($offset !== 0){
240 5
            $sql .= 'AND `items`.`id` ' .
241
                $this->getOperator($oldestFirst) . ' ? ';
242 5
            $params[] = $offset;
243
        }
244
        $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst,
245
                                            $search);
246 1
247 1
        return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
248 1
    }
249 1
250 1
251 1
    public function findAllUnreadOrStarred($userId) {
252
        $params = [$userId];
253
        $status = StatusFlag::UNREAD | StatusFlag::STARRED;
254
        $sql = 'AND ((`items`.`status` & ' . $status . ') > 0) ';
255 1
        $sql = $this->makeSelectQuery($sql);
256 1
        return $this->findEntities($sql, $params);
257
    }
258 1
259
260 1
    public function findByGuidHash($guidHash, $feedId, $userId){
261
        $sql = $this->makeSelectQuery(
262
            'AND `items`.`guid_hash` = ? ' .
263
            'AND `feeds`.`id` = ? ');
264
265
        return $this->findEntity($sql, [$userId, $guidHash, $feedId]);
266
    }
267
268
269 2
    /**
270 2
     * Delete all items for feeds that have over $threshold unread and not
271 2
     * 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 2
278 2
        $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
            'JOIN `*PREFIX*news_feeds` `feeds` ' .
282 2
                'ON `feeds`.`id` = `items`.`feed_id` ' .
283
                'AND NOT ((`items`.`status` & ?) > 0) ' .
284 2
            'GROUP BY `feeds`.`id`, `feeds`.`articles_per_update` ' .
285
            'HAVING COUNT(*) > ?';
286 2
287 2
        $result = $this->execute($sql, $params);
288
289 2
        while($row = $result->fetch()) {
290 1
291
            $size = (int) $row['size'];
292
            $limit = $size - $threshold;
293 1
294 1
            if($limit > 0) {
295 1
                $params = [$status, $row['feed_id'], $limit];
296 1
297 1
                $sql = 'DELETE FROM `*PREFIX*news_items` ' .
298 1
                'WHERE `id` IN (' .
299 1
                    'SELECT `id` FROM `*PREFIX*news_items` ' .
300
                    'WHERE NOT ((`status` & ?) > 0) ' .
301 1
                    'AND `feed_id` = ? ' .
302 1
                    'ORDER BY `id` ASC ' .
303 2
                    'LIMIT ?' .
304
                ')';
305 2
306
                $this->execute($sql, $params);
307
            }
308 2
        }
309
310 2
    }
311 2
312 2
313 2
    public function getNewestItemId($userId) {
314 2
        $sql = 'SELECT MAX(`items`.`id`) AS `max_id` ' .
315
            'FROM `*PREFIX*news_items` `items` '.
316 2
            'JOIN `*PREFIX*news_feeds` `feeds` ' .
317
                'ON `feeds`.`id` = `items`.`feed_id` '.
318 1
                'AND `feeds`.`user_id` = ?';
319
        $params = [$userId];
320
321
        $result = $this->findOneQuery($sql, $params);
322
323
        return (int) $result['max_id'];
324
    }
325
326 1
327
    /**
328 1
     * Deletes all items of a user
329 1
     * @param string $userId the name of the user
330 1
     */
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
                    'WHERE `feeds`.`user_id` = ?' .
336
                ')';
337
338
        $this->execute($sql, [$userId]);
339
    }
340
341
342
    /**
343
     * Returns a list of ids and userid of all items
344
     */
345
    public function findAllItemIdsAndUsers($limit=null, $offset=null) {
346
        $sql = 'SELECT `items`.`id`, `feeds`.`user_id` ' .
347
                'FROM `*PREFIX*news_items` `items` ' .
348
                'JOIN `*PREFIX*news_feeds` `feeds` ' .
349
                    'ON `items`.`feed_id` = `feeds`.`id`';
350
        return $this->execute($sql, [], $limit, $offset)->fetchAll();
351
    }
352
353
    /**
354
     * Update search indices of all items
355
     */
356
    public function updateSearchIndices() {
357
        // update indices in steps to prevent memory issues on larger systems
358
        $step = 1000;  // update 1000 items at a time
359
        $itemCount = 1;
360
        $offset = 0;
361
362
        // stop condition if there are no previously fetched items
363
        while ($itemCount > 0) {
364
            $items = $this->findAllItemIdsAndUsers($step, $offset);
365
            $itemCount = count($items);
366
            $this->updateSearchIndex($items);
367
            $offset += $step;
368
        }
369
    }
370
371
    private function updateSearchIndex(array $items=[]) {
372
        foreach ($items as $row) {
373
            // ignore items of deleted rows
374
            try {
375
                $item = $this->find($row['id'], $row['user_id']);
376
            } catch(\Exception $e) {
377
                continue;
378
            }
379
380
            $item->generateSearchIndex();
381
            $this->update($item);
382
        }
383
    }
384
385
386
}
387