1 | <?php |
||
20 | class ItemMapper extends NewsMapper { |
||
21 | |||
22 | 16 | public function __construct(IDBConnection $db) { |
|
23 | 16 | parent::__construct($db, 'news_items', Item::class); |
|
24 | 16 | } |
|
25 | |||
26 | |||
27 | 11 | private function makeSelectQuery($prependTo = '', $oldestFirst = false, |
|
28 | $distinctFingerprint = false) { |
||
29 | 11 | if ($oldestFirst) { |
|
30 | $ordering = 'ASC'; |
||
31 | } else { |
||
32 | 11 | $ordering = 'DESC'; |
|
33 | } |
||
34 | |||
35 | return 'SELECT `items`.* FROM `*PREFIX*news_items` `items` ' . |
||
36 | 11 | 'JOIN `*PREFIX*news_feeds` `feeds` ' . |
|
37 | 11 | 'ON `feeds`.`id` = `items`.`feed_id` ' . |
|
38 | 11 | 'AND `feeds`.`deleted_at` = 0 ' . |
|
39 | 11 | 'AND `feeds`.`user_id` = ? ' . |
|
40 | 11 | $prependTo . |
|
41 | 11 | 'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' . |
|
42 | 11 | 'ON `folders`.`id` = `feeds`.`folder_id` ' . |
|
43 | 11 | 'WHERE `feeds`.`folder_id` = 0 ' . |
|
44 | 11 | 'OR `folders`.`deleted_at` = 0 ' . |
|
45 | 11 | 'ORDER BY `items`.`id` ' . $ordering; |
|
46 | } |
||
47 | |||
48 | 3 | private function makeSelectQueryStatus($prependTo, $status, |
|
49 | $oldestFirst = false, $search = [], |
||
50 | $distinctFingerprint = false) { |
||
51 | 3 | $status = (int)$status; |
|
52 | 3 | $count = count($search); |
|
53 | |||
54 | // WARNING: Potential SQL injection if you change this carelessly |
||
55 | 3 | $sql = 'AND ((`items`.`status` & ' . $status . ') = ' . $status . ') '; |
|
56 | 3 | $sql .= str_repeat('AND `items`.`search_index` LIKE ? ', $count); |
|
57 | 3 | $sql .= $prependTo; |
|
58 | |||
59 | 3 | 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 | 3 | return array_map(function ($param) { |
|
70 | $param = addcslashes($param, '\\_%'); |
||
71 | return '%' . mb_strtolower($param, 'UTF-8') . '%'; |
||
72 | 3 | }, $search); |
|
73 | } |
||
74 | |||
75 | /** |
||
76 | * @param int $id |
||
77 | * @param string $userId |
||
78 | * @return \OCA\News\Db\Item |
||
79 | */ |
||
80 | 10 | public function find($id, $userId) { |
|
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 | private function getOperator($oldestFirst) { |
||
154 | if ($oldestFirst) { |
||
155 | return '>'; |
||
156 | } else { |
||
157 | return '<'; |
||
158 | } |
||
159 | } |
||
160 | |||
161 | |||
162 | public function findAllNew($updatedSince, $status, $userId) { |
||
163 | $sql = $this->makeSelectQueryStatus( |
||
164 | 'AND `items`.`last_modified` >= ? ', $status); |
||
165 | $params = [$userId, $updatedSince]; |
||
166 | return $this->findEntities($sql, $params); |
||
167 | } |
||
168 | |||
169 | |||
170 | 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 | $params = [$userId, $id, $updatedSince]; |
||
175 | return $this->findEntities($sql, $params); |
||
176 | } |
||
177 | |||
178 | |||
179 | 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 | $params = [$userId, $id, $updatedSince]; |
||
184 | return $this->findEntities($sql, $params); |
||
185 | } |
||
186 | |||
187 | |||
188 | 3 | private function findEntitiesIgnoringNegativeLimit($sql, $params, $limit) { |
|
189 | // ignore limit if negative to offer a way to return all feeds |
||
190 | 3 | if ($limit >= 0) { |
|
191 | 3 | return $this->findEntities($sql, $params, $limit); |
|
192 | } else { |
||
193 | return $this->findEntities($sql, $params); |
||
194 | } |
||
195 | } |
||
196 | |||
197 | |||
198 | public function findAllFeed($id, $limit, $offset, $status, $oldestFirst, |
||
199 | $userId, $search = []) { |
||
200 | $params = [$userId]; |
||
201 | $params = array_merge($params, $this->buildLikeParameters($search)); |
||
202 | $params[] = $id; |
||
203 | |||
204 | $sql = 'AND `items`.`feed_id` = ? '; |
||
205 | if ($offset !== 0) { |
||
206 | $sql .= 'AND `items`.`id` ' . |
||
207 | $this->getOperator($oldestFirst) . ' ? '; |
||
208 | $params[] = $offset; |
||
209 | } |
||
210 | $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst, |
||
211 | $search); |
||
212 | return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit); |
||
213 | } |
||
214 | |||
215 | |||
216 | public function findAllFolder($id, $limit, $offset, $status, $oldestFirst, |
||
217 | $userId, $search = []) { |
||
218 | $params = [$userId]; |
||
219 | $params = array_merge($params, $this->buildLikeParameters($search)); |
||
220 | $params[] = $id; |
||
221 | |||
222 | $sql = 'AND `feeds`.`folder_id` = ? '; |
||
223 | if ($offset !== 0) { |
||
224 | $sql .= 'AND `items`.`id` ' . |
||
225 | $this->getOperator($oldestFirst) . ' ? '; |
||
226 | $params[] = $offset; |
||
227 | } |
||
228 | $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst, |
||
229 | $search); |
||
230 | return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit); |
||
231 | } |
||
232 | |||
233 | |||
234 | 3 | public function findAll($limit, $offset, $status, $oldestFirst, $userId, |
|
235 | $search = []) { |
||
236 | 3 | $params = [$userId]; |
|
237 | 3 | $params = array_merge($params, $this->buildLikeParameters($search)); |
|
238 | 3 | $sql = ''; |
|
239 | 3 | if ($offset !== 0) { |
|
240 | $sql .= 'AND `items`.`id` ' . |
||
241 | $this->getOperator($oldestFirst) . ' ? '; |
||
242 | $params[] = $offset; |
||
243 | } |
||
244 | 3 | $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst, |
|
245 | 3 | $search); |
|
246 | |||
247 | 3 | 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 | 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 | /** |
||
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 | 2 | $params = [$status, $row['feed_id'], $limit]; |
|
296 | |||
297 | $sql = 'DELETE FROM `*PREFIX*news_items` ' . |
||
298 | 2 | 'WHERE `id` IN (' . |
|
299 | 2 | 'SELECT `id` FROM `*PREFIX*news_items` ' . |
|
300 | 2 | 'WHERE NOT ((`status` & ?) > 0) ' . |
|
301 | 2 | 'AND `feed_id` = ? ' . |
|
302 | 2 | 'ORDER BY `id` ASC ' . |
|
303 | 2 | 'LIMIT ?' . |
|
304 | 2 | ')'; |
|
305 | |||
306 | 2 | $this->execute($sql, $params); |
|
307 | 2 | } |
|
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 | 2 | 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) { |
||
349 | |||
350 | /** |
||
351 | * Update search indices of all items |
||
352 | */ |
||
353 | public function updateSearchIndices() { |
||
367 | |||
368 | private function updateSearchIndex(array $items = []) { |
||
369 | foreach ($items as $row) { |
||
370 | $sql = 'SELECT * FROM `*PREFIX*news_items` WHERE `id` = ?'; |
||
371 | $params = [$row['id']]; |
||
372 | $item = $this->findEntity($sql, $params); |
||
373 | $item->generateSearchIndex(); |
||
374 | $this->update($item); |
||
375 | } |
||
376 | } |
||
377 | |||
378 | 2 | public function readItem($itemId, $isRead, $lastModified, $userId) { |
|
379 | 2 | $item = $this->find($itemId, $userId); |
|
380 | |||
381 | // reading an item should set all of the same items as read, whereas |
||
382 | // marking an item as unread should only mark the selected instance |
||
383 | // as unread |
||
384 | 2 | if ($isRead) { |
|
385 | $sql = 'UPDATE `*PREFIX*news_items` |
||
386 | SET `status` = `status` & ?, |
||
387 | `last_modified` = ? |
||
388 | WHERE `fingerprint` = ? |
||
389 | AND `feed_id` IN ( |
||
402 | |||
403 | } |
||
404 |