The thread table is replaced by post-thread and post-thread-user
[friendica.git/.git] / src / Model / Post.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Model;
23
24 use Friendica\Core\Logger;
25 use Friendica\Database\DBA;
26 use Friendica\Database\DBStructure;
27 use Friendica\Protocol\Activity;
28
29 class Post
30 {
31         /**
32          * Fetch a single post row
33          *
34          * @param mixed $stmt statement object
35          * @return array|false current row or false
36          * @throws \Exception
37          */
38         public static function fetch($stmt)
39         {
40                 $row = DBA::fetch($stmt);
41
42                 if (!is_array($row)) {
43                         return $row;
44                 }
45
46                 if (array_key_exists('verb', $row)) {
47                         if (in_array($row['verb'], Item::ACTIVITIES)) {
48                                 if (array_key_exists('title', $row)) {
49                                         $row['title'] = '';
50                                 }
51                                 if (array_key_exists('body', $row)) {
52                                         $row['body'] = $row['verb'];
53                                 }
54                                 if (array_key_exists('object', $row)) {
55                                         $row['object'] = '';
56                                 }
57                                 if (array_key_exists('object-type', $row)) {
58                                         $row['object-type'] = Activity\ObjectType::NOTE;
59                                 }
60                         } elseif (in_array($row['verb'], ['', Activity::POST, Activity::SHARE])) {
61                                 // Posts don't have a target - but having tags or files.
62                                 if (array_key_exists('target', $row)) {
63                                         $row['target'] = '';
64                                 }
65                         }
66                 }
67
68                 return $row;
69         }
70
71         /**
72          * Fills an array with data from an post query
73          *
74          * @param object $stmt statement object
75          * @param bool   $do_close
76          * @return array Data array
77          */
78         public static function toArray($stmt, $do_close = true) {
79                 if (is_bool($stmt)) {
80                         return $stmt;
81                 }
82
83                 $data = [];
84                 while ($row = self::fetch($stmt)) {
85                         $data[] = $row;
86                 }
87                 if ($do_close) {
88                         DBA::close($stmt);
89                 }
90                 return $data;
91         }
92
93         /**
94          * Check if post data exists
95          *
96          * @param array $condition array of fields for condition
97          *
98          * @return boolean Are there rows for that condition?
99          * @throws \Exception
100          */
101         public static function exists($condition) {
102                 return DBA::exists('post-view', $condition);
103         }
104
105         /**
106          * Counts the posts satisfying the provided condition
107          *
108          * @param array        $condition array of fields for condition
109          * @param array        $params    Array of several parameters
110          *
111          * @return int
112          *
113          * Example:
114          * $condition = ["uid" => 1, "network" => 'dspr'];
115          * or:
116          * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
117          *
118          * $count = Post::count($condition);
119          * @throws \Exception
120          */
121         public static function count(array $condition = [], array $params = [])
122         {
123                 return DBA::count('post-view', $condition, $params);
124         }
125
126         /**
127          * Retrieve a single record from the post table and returns it in an associative array
128          *
129          * @param array $fields
130          * @param array $condition
131          * @param array $params
132          * @return bool|array
133          * @throws \Exception
134          * @see   DBA::select
135          */
136         public static function selectFirst(array $fields = [], array $condition = [], $params = [])
137         {
138                 $params['limit'] = 1;
139
140                 $result = self::select($fields, $condition, $params);
141
142                 if (is_bool($result)) {
143                         return $result;
144                 } else {
145                         $row = self::fetch($result);
146                         DBA::close($result);
147                         return $row;
148                 }
149         }
150
151         /**
152          * Select rows from the post table and returns them as an array
153          *
154          * @param array $selected  Array of selected fields, empty for all
155          * @param array $condition Array of fields for condition
156          * @param array $params    Array of several parameters
157          *
158          * @return array
159          * @throws \Exception
160          */
161         public static function selectToArray(array $fields = [], array $condition = [], $params = [])
162         {
163                 $result = self::select($fields, $condition, $params);
164
165                 if (is_bool($result)) {
166                         return [];
167                 }
168
169                 $data = [];
170                 while ($row = self::fetch($result)) {
171                         $data[] = $row;
172                 }
173                 DBA::close($result);
174
175                 return $data;
176         }
177
178         /**
179          * Select rows from the given view
180          *
181          * @param string $view      View (post-view or post-thread-view)
182          * @param array  $selected  Array of selected fields, empty for all
183          * @param array  $condition Array of fields for condition
184          * @param array  $params    Array of several parameters
185          *
186          * @return boolean|object
187          * @throws \Exception
188          */
189         private static function selectView(string $view, array $selected = [], array $condition = [], $params = [])
190         {
191                 if (empty($selected)) {
192                         $selected = array_merge(['author-addr', 'author-nick', 'owner-addr', 'owner-nick', 'causer-addr', 'causer-nick',
193                                 'causer-network', 'photo', 'name-date', 'uri-date', 'avatar-date', 'thumb', 'dfrn-id',
194                                 'parent-guid', 'parent-network', 'parent-author-id', 'parent-author-link', 'parent-author-name',
195                                 'parent-author-network', 'signed_text', 'language', 'raw-body'], Item::DISPLAY_FIELDLIST, Item::ITEM_FIELDLIST);
196                         
197                         if ($view != 'post-view') {
198                                 $selected = array_merge($selected, ['ignored', 'iid']);
199                         }
200                 }
201
202                 $selected = array_unique($selected);
203
204                 return DBA::select($view, $selected, $condition, $params);
205         }
206
207         /**
208          * Select rows from the post table
209          *
210          * @param array $selected  Array of selected fields, empty for all
211          * @param array $condition Array of fields for condition
212          * @param array $params    Array of several parameters
213          *
214          * @return boolean|object
215          * @throws \Exception
216          */
217         public static function select(array $selected = [], array $condition = [], $params = [])
218         {
219                 return self::selectView('post-view', $selected, $condition, $params);
220         }
221
222         /**
223          * Select rows from the post table
224          *
225          * @param array $selected  Array of selected fields, empty for all
226          * @param array $condition Array of fields for condition
227          * @param array $params    Array of several parameters
228          *
229          * @return boolean|object
230          * @throws \Exception
231          */
232         public static function selectThread(array $selected = [], array $condition = [], $params = [])
233         {
234                 return self::selectView('post-thread-view', $selected, $condition, $params);
235         }
236
237         /**
238          * Select rows from the given view for a given user
239          *
240          * @param string  $view      View (post-view or post-thread-view)
241          * @param integer $uid       User ID
242          * @param array   $selected  Array of selected fields, empty for all
243          * @param array   $condition Array of fields for condition
244          * @param array   $params    Array of several parameters
245          *
246          * @return boolean|object
247          * @throws \Exception
248          */
249         private static function selectViewForUser(string $view, $uid, array $selected = [], array $condition = [], $params = [])
250         {
251                 if (empty($selected)) {
252                         $selected = Item::DISPLAY_FIELDLIST;
253                 }
254
255                 $condition = DBA::mergeConditions($condition,
256                         ["`visible` AND NOT `deleted` AND NOT `moderated`
257                         AND NOT `author-blocked` AND NOT `owner-blocked`
258                         AND (NOT `causer-blocked` OR `causer-id` = ?) AND NOT `contact-blocked`
259                         AND ((NOT `contact-readonly` AND NOT `contact-pending` AND (`contact-rel` IN (?, ?)))
260                                 OR `self` OR `gravity` != ? OR `contact-uid` = ?)
261                         AND NOT EXISTS (SELECT `uri-id` FROM `post-user` WHERE `hidden` AND `uri-id` = `" . $view . "`.`uri-id` AND `uid` = ?)
262                         AND NOT EXISTS (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `author-id` AND `blocked`)
263                         AND NOT EXISTS (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `owner-id` AND `blocked`)
264                         AND NOT EXISTS (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `author-id` AND `ignored` AND `gravity` = ?)
265                         AND NOT EXISTS (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `owner-id` AND `ignored` AND `gravity` = ?)",
266                         0, Contact::SHARING, Contact::FRIEND, GRAVITY_PARENT, 0, $uid, $uid, $uid, $uid, GRAVITY_PARENT, $uid, GRAVITY_PARENT]);
267
268                 $select_string = '';
269
270                 if (in_array('pinned', $selected)) {
271                         $selected = array_flip($selected);
272                         unset($selected['pinned']);
273                         $selected = array_flip($selected);      
274
275                         $select_string = "(SELECT `pinned` FROM `post-thread-user` WHERE `uri-id` = `" . $view . "`.`uri-id` AND uid=`" . $view . "`.`uid`) AS `pinned`, ";
276                 }
277
278                 $select_string .= implode(', ', array_map([DBA::class, 'quoteIdentifier'], $selected));
279
280                 $condition_string = DBA::buildCondition($condition);
281                 $param_string = DBA::buildParameter($params);
282
283                 $sql = "SELECT " . $select_string . " FROM `" . $view . "` " . $condition_string . $param_string;
284                 $sql = DBA::cleanQuery($sql);
285
286                 return DBA::p($sql, $condition);
287         }
288
289         /**
290          * Select rows from the post view for a given user
291          *
292          * @param integer $uid       User ID
293          * @param array   $selected  Array of selected fields, empty for all
294          * @param array   $condition Array of fields for condition
295          * @param array   $params    Array of several parameters
296          *
297          * @return boolean|object
298          * @throws \Exception
299          */
300         public static function selectForUser($uid, array $selected = [], array $condition = [], $params = [])
301         {
302                 return self::selectViewForUser('post-view', $uid, $selected, $condition, $params);
303         }
304
305                 /**
306          * Select rows from the post view for a given user
307          *
308          * @param integer $uid       User ID
309          * @param array   $selected  Array of selected fields, empty for all
310          * @param array   $condition Array of fields for condition
311          * @param array   $params    Array of several parameters
312          *
313          * @return boolean|object
314          * @throws \Exception
315          */
316         public static function selectThreadForUser($uid, array $selected = [], array $condition = [], $params = [])
317         {
318                 return self::selectViewForUser('post-thread-view', $uid, $selected, $condition, $params);
319         }
320
321         /**
322          * Retrieve a single record from the post view for a given user and returns it in an associative array
323          *
324          * @param integer $uid User ID
325          * @param array   $selected
326          * @param array   $condition
327          * @param array   $params
328          * @return bool|array
329          * @throws \Exception
330          * @see   DBA::select
331          */
332         public static function selectFirstForUser($uid, array $selected = [], array $condition = [], $params = [])
333         {
334                 $params['limit'] = 1;
335
336                 $result = self::selectForUser($uid, $selected, $condition, $params);
337
338                 if (is_bool($result)) {
339                         return $result;
340                 } else {
341                         $row = self::fetch($result);
342                         DBA::close($result);
343                         return $row;
344                 }
345         }
346
347         /**
348          * Select pinned rows from the item table for a given user
349          *
350          * @param integer $uid       User ID
351          * @param array   $selected  Array of selected fields, empty for all
352          * @param array   $condition Array of fields for condition
353          * @param array   $params    Array of several parameters
354          *
355          * @return boolean|object
356          * @throws \Exception
357          */
358         public static function selectPinned(int $uid, array $selected = [], array $condition = [], $params = [])
359         {
360                 $postthreaduser = DBA::select('post-thread-user', ['uri-id'], ['uid' => $uid, 'pinned' => true]);
361                 if (!DBA::isResult($postthreaduser)) {
362                         return $postthreaduser;
363                 }
364         
365                 $pinned = [];
366                 while ($useritem = DBA::fetch($postthreaduser)) {
367                         $pinned[] = $useritem['uri-id'];
368                 }
369                 DBA::close($postthreaduser);
370
371                 if (empty($pinned)) {
372                         return [];
373                 }
374
375                 $condition = DBA::mergeConditions(['uri-id' => $pinned, 'uid' => $uid, 'gravity' => GRAVITY_PARENT], $condition);
376
377                 return self::selectForUser($uid, $selected, $condition, $params);
378         }
379
380         /**
381          * Update existing post entries
382          *
383          * @param array $fields    The fields that are to be changed
384          * @param array $condition The condition for finding the item entries
385          *
386          * A return value of "0" doesn't mean an error - but that 0 rows had been changed.
387          *
388          * @return integer|boolean number of affected rows - or "false" if there was an error
389          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
390          */
391         public static function update(array $fields, array $condition)
392         {
393                 $affected = 0;
394
395                 Logger::info('Start Update', ['fields' => $fields, 'condition' => $condition]);
396
397                 // Don't allow changes to fields that are responsible for the relation between the records
398                 unset($fields['id']);
399                 unset($fields['parent']);
400                 unset($fields['uid']);
401                 unset($fields['uri']);
402                 unset($fields['uri-id']);
403                 unset($fields['thr-parent']);
404                 unset($fields['thr-parent-id']);
405                 unset($fields['parent-uri']);
406                 unset($fields['parent-uri-id']);
407
408                 $thread_condition = DBA::mergeConditions($condition, ['gravity' => GRAVITY_PARENT]);
409
410                 // To ensure the data integrity we do it in an transaction
411                 DBA::transaction();
412
413                 $update_fields = DBStructure::getFieldsForTable('post-user', $fields);
414                 if (!empty($update_fields)) {
415                         $rows = DBA::selectToArray('post-view', ['post-user-id'], $condition);
416                         $puids = array_column($rows, 'post-user-id');
417                         if (!DBA::update('post-user', $update_fields, ['id' => $puids])) {
418                                 DBA::rollback();
419                                 Logger::notice('Updating post-user failed', ['fields' => $update_fields, 'condition' => $condition]);
420                                 return false;
421                         }
422                         $affected = DBA::affectedRows();                        
423                 }
424
425                 $update_fields = DBStructure::getFieldsForTable('post-content', $fields);
426                 if (!empty($update_fields)) {
427                         $rows = DBA::selectToArray('post-view', ['uri-id'], $condition, ['group_by' => ['uri-id']]);
428                         $uriids = array_column($rows, 'uri-id');
429                         if (!DBA::update('post-content', $update_fields, ['uri-id' => $uriids])) {
430                                 DBA::rollback();
431                                 Logger::notice('Updating post-content failed', ['fields' => $update_fields, 'condition' => $condition]);
432                                 return false;
433                         }
434                         $affected = max($affected, DBA::affectedRows());
435                 }
436
437                 $update_fields = Post\DeliveryData::extractFields($fields);
438                 if (!empty($update_fields)) {
439                         if (empty($uriids)) {
440                                 $rows = DBA::selectToArray('post-view', ['uri-id'], $condition, ['group_by' => ['uri-id']]);
441                                 $uriids = array_column($rows, 'uri-id');
442                         }
443                         if (!DBA::update('post-delivery-data', $update_fields, ['uri-id' => $uriids])) {
444                                 DBA::rollback();
445                                 Logger::notice('Updating post-delivery-data failed', ['fields' => $update_fields, 'condition' => $condition]);
446                                 return false;
447                         }
448                         $affected = max($affected, DBA::affectedRows());
449                 }
450
451                 $update_fields = DBStructure::getFieldsForTable('post-thread', $fields);
452                 if (!empty($update_fields)) {
453                         $rows = DBA::selectToArray('post-view', ['uri-id'], $condition, ['group_by' => ['uri-id']]);
454                         $uriids = array_column($rows, 'uri-id');
455                         if (!DBA::update('post-thread', $update_fields, ['uri-id' => $uriids])) {
456                                 DBA::rollback();
457                                 Logger::notice('Updating post-thread failed', ['fields' => $update_fields, 'condition' => $condition]);
458                                 return false;
459                         }
460                         $affected = max($affected, DBA::affectedRows());
461                 }
462
463                 $update_fields = DBStructure::getFieldsForTable('post-thread-user', $fields);
464                 if (!empty($update_fields)) {
465                         $rows = DBA::selectToArray('post-view', ['post-user-id'], $thread_condition);
466                         $thread_puids = array_column($rows, 'post-user-id');
467
468                         $post_thread_condition = DBA::collapseCondition(['id' => $thread_puids]);
469
470                         $post_thread_condition[0] = "EXISTS(SELECT `id` FROM `post-user` WHERE " .
471                                 $post_thread_condition[0] . " AND `uri-id` = `post-thread-user`.`uri-id` AND `uid` = `post-thread-user`.`uid`)";
472                                 if (!DBA::update('post-thread-user', $update_fields, $post_thread_condition)) {
473                                 DBA::rollback();
474                                 Logger::notice('Updating post-thread-user failed', ['fields' => $update_fields, 'condition' => $condition]);
475                                 return false;
476                         }
477                         $affected = max($affected, DBA::affectedRows());
478                 }
479
480                 $update_fields = DBStructure::getFieldsForTable('thread', $fields);
481                 if (!empty($update_fields)) {
482                         $rows = DBA::selectToArray('post-view', ['id'], $thread_condition);
483                         $ids = array_column($rows, 'id');
484                         if (!DBA::update('thread', $update_fields, ['iid' => $ids])) {
485                                 DBA::rollback();
486                                 Logger::notice('Updating thread failed', ['fields' => $update_fields, 'condition' => $thread_condition]);
487                                 return false;
488                         }
489                         $affected = max($affected, DBA::affectedRows());
490                 }
491
492                 $update_fields = [];
493                 foreach (Item::USED_FIELDLIST as $field) {
494                         if (array_key_exists($field, $fields)) {
495                                 $update_fields[$field] = $fields[$field];
496                         }
497                 }
498                 if (!empty($update_fields)) {
499                         $rows = DBA::selectToArray('post-view', ['id'], $condition, []);
500                         $ids = array_column($rows, 'id');
501                         if (!DBA::update('item', $update_fields, ['id' => $ids])) {
502                                 DBA::rollback();
503                                 Logger::notice('Updating item failed', ['fields' => $update_fields, 'condition' => $condition]);
504                                 return false;
505                         }
506                         $affected = max($affected, DBA::affectedRows());
507                 }
508
509                 DBA::commit();
510
511                 Logger::info('Updated posts', ['rows' => $affected]);
512                 return $affected;
513         }
514 }