]> git.mxchange.org Git - friendica.git/blob - src/Model/Post.php
Changes:
[friendica.git] / src / Model / Post.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
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 BadMethodCallException;
25 use Friendica\Core\Logger;
26 use Friendica\Core\System;
27 use Friendica\Database\Database;
28 use Friendica\Database\DBA;
29 use Friendica\DI;
30 use Friendica\Protocol\Activity;
31
32 class Post
33 {
34         /**
35          * Insert a new post entry
36          *
37          * @param integer $uri_id
38          * @param array   $fields
39          * @return int    ID of inserted post
40          * @throws \Exception
41          */
42         public static function insert(int $uri_id, array $data = []): int
43         {
44                 if (empty($uri_id)) {
45                         throw new BadMethodCallException('Empty URI_id');
46                 }
47
48                 $fields = DI::dbaDefinition()->truncateFieldsForTable('post', $data);
49
50                 // Additionally assign the key fields
51                 $fields['uri-id'] = $uri_id;
52
53                 if (!DBA::insert('post', $fields, Database::INSERT_IGNORE)) {
54                         return 0;
55                 }
56
57                 return DBA::lastInsertId();
58         }
59
60         /**
61          * Fetch a single post row
62          *
63          * @param mixed $stmt statement object
64          * @return array|false current row or false
65          * @throws \Exception
66          */
67         public static function fetch($stmt)
68         {
69                 $row = DBA::fetch($stmt);
70
71                 if (!is_array($row)) {
72                         return $row;
73                 }
74
75                 if (array_key_exists('verb', $row)) {
76                         if (in_array($row['verb'], Item::ACTIVITIES)) {
77                                 if (array_key_exists('title', $row)) {
78                                         $row['title'] = '';
79                                 }
80                                 if (array_key_exists('body', $row)) {
81                                         $row['body'] = $row['verb'];
82                                 }
83                                 if (array_key_exists('object', $row)) {
84                                         $row['object'] = '';
85                                 }
86                                 if (array_key_exists('object-type', $row)) {
87                                         $row['object-type'] = Activity\ObjectType::NOTE;
88                                 }
89                         } elseif (in_array($row['verb'], ['', Activity::POST, Activity::SHARE])) {
90                                 // Posts don't have a target - but having tags or files.
91                                 if (array_key_exists('target', $row)) {
92                                         $row['target'] = '';
93                                 }
94                         }
95                 }
96
97                 if (array_key_exists('extid', $row) && is_null($row['extid'])) {
98                         $row['extid'] = '';
99                 }
100
101                 return $row;
102         }
103
104         /**
105          * Fills an array with data from an post query
106          *
107          * @param object $stmt statement object
108          * @param bool   $do_close
109          * @return array Data array
110          * @todo Find proper type-hint for $stmt and maybe avoid boolean
111          */
112         public static function toArray($stmt, bool $do_close = true)
113         {
114                 if (is_bool($stmt)) {
115                         return $stmt;
116                 }
117
118                 $data = [];
119                 while ($row = self::fetch($stmt)) {
120                         $data[] = $row;
121                 }
122                 if ($do_close) {
123                         DBA::close($stmt);
124                 }
125                 return $data;
126         }
127
128         /**
129          * Check if post-user-view records exists
130          *
131          * @param array $condition array of fields for condition
132          *
133          * @return boolean Are there rows for that condition?
134          * @throws \Exception
135          */
136         public static function exists(array $condition): bool
137         {
138                 return DBA::exists('post-user-view', $condition);
139         }
140
141         /**
142          * Counts the post-user-view records satisfying the provided condition
143          *
144          * @param array        $condition array of fields for condition
145          * @param array        $params    Array of several parameters
146          *
147          * @return int
148          *
149          * Example:
150          * $condition = ["uid" => 1, "network" => 'dspr'];
151          * or:
152          * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
153          *
154          * $count = Post::count($condition);
155          * @throws \Exception
156          */
157         public static function count(array $condition = [], array $params = []): int
158         {
159                 return DBA::count('post-user-view', $condition, $params);
160         }
161
162         /**
163          * Counts the post-thread-user-view records satisfying the provided condition
164          *
165          * @param array        $condition array of fields for condition
166          * @param array        $params    Array of several parameters
167          *
168          * @return int
169          *
170          * Example:
171          * $condition = ["uid" => 1, "network" => 'dspr'];
172          * or:
173          * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
174          *
175          * $count = Post::count($condition);
176          * @throws \Exception
177          */
178         public static function countThread(array $condition = [], array $params = []): int
179         {
180                 return DBA::count('post-thread-user-view', $condition, $params);
181         }
182
183         /**
184          * Counts the post-view records satisfying the provided condition
185          *
186          * @param array        $condition array of fields for condition
187          * @param array        $params    Array of several parameters
188          *
189          * @return int
190          *
191          * Example:
192          * $condition = ["network" => 'dspr'];
193          * or:
194          * $condition = ["`network` IN (?, ?)", 1, 'dfrn', 'dspr'];
195          *
196          * $count = Post::count($condition);
197          * @throws \Exception
198          */
199         public static function countPosts(array $condition = [], array $params = []): int
200         {
201                 return DBA::count('post-view', $condition, $params);
202         }
203
204         /**
205          * Retrieve a single record from the post-user-view view and returns it in an associative array
206          *
207          * @param array $fields
208          * @param array $condition
209          * @param array $params
210          * @param bool  $user_mode true = post-user-view, false = post-view
211          * @return bool|array
212          * @throws \Exception
213          * @see   DBA::select
214          */
215         public static function selectFirst(array $fields = [], array $condition = [], array $params = [])
216         {
217                 $params['limit'] = 1;
218
219                 $result = self::select($fields, $condition, $params);
220
221                 if (is_bool($result)) {
222                         return $result;
223                 } else {
224                         $row = self::fetch($result);
225                         DBA::close($result);
226                         return $row;
227                 }
228         }
229
230         /**
231          * Retrieve a single record from the post-view view and returns it in an associative array
232          *
233          * @param array $fields
234          * @param array $condition
235          * @param array $params
236          * @return bool|array
237          * @throws \Exception
238          * @see   DBA::select
239          */
240         public static function selectFirstPost(array $fields = [], array $condition = [], array $params = [])
241         {
242                 $params['limit'] = 1;
243
244                 $result = self::selectPosts($fields, $condition, $params);
245
246                 if (is_bool($result)) {
247                         return $result;
248                 } else {
249                         $row = self::fetch($result);
250                         DBA::close($result);
251                         return $row;
252                 }
253         }
254
255         /**
256          * Retrieve a single record from the post-thread-user-view view and returns it in an associative array
257          *
258          * @param array $fields
259          * @param array $condition
260          * @param array $params
261          * @return bool|array
262          * @throws \Exception
263          * @see   DBA::select
264          */
265         public static function selectFirstThread(array $fields = [], array $condition = [], array $params = [])
266         {
267                 $params['limit'] = 1;
268
269                 $result = self::selectThread($fields, $condition, $params);
270
271                 if (is_bool($result)) {
272                         return $result;
273                 } else {
274                         $row = self::fetch($result);
275                         DBA::close($result);
276                         return $row;
277                 }
278         }
279
280         /**
281          * Select rows from the post-user-view view and returns them as an array
282          *
283          * @param array $selected  Array of selected fields, empty for all
284          * @param array $condition Array of fields for condition
285          * @param array $params    Array of several parameters
286          *
287          * @return array
288          * @throws \Exception
289          */
290         public static function selectToArray(array $fields = [], array $condition = [], array $params = [])
291         {
292                 $result = self::select($fields, $condition, $params);
293
294                 if (is_bool($result)) {
295                         return [];
296                 }
297
298                 $data = [];
299                 while ($row = self::fetch($result)) {
300                         $data[] = $row;
301                 }
302                 DBA::close($result);
303
304                 return $data;
305         }
306
307         /**
308          * Select rows from the given view
309          *
310          * @param string $view      View (post-user-view or post-thread-user-view)
311          * @param array  $selected  Array of selected fields, empty for all
312          * @param array  $condition Array of fields for condition
313          * @param array  $params    Array of several parameters
314          *
315          * @return boolean|object
316          * @throws \Exception
317          */
318         private static function selectView(string $view, array $selected = [], array $condition = [], array $params = [])
319         {
320                 if (empty($selected)) {
321                         $selected = array_merge(Item::DISPLAY_FIELDLIST, Item::ITEM_FIELDLIST);
322
323                         if ($view == 'post-thread-user-view') {
324                                 $selected = array_merge($selected, ['ignored']);
325                         }
326                 }
327
328                 $selected = array_unique($selected);
329
330                 return DBA::select($view, $selected, $condition, $params);
331         }
332
333         /**
334          * Select rows from the post-user-view view
335          *
336          * @param array $selected  Array of selected fields, empty for all
337          * @param array $condition Array of fields for condition
338          * @param array $params    Array of several parameters
339          *
340          * @return boolean|object
341          * @throws \Exception
342          */
343         public static function select(array $selected = [], array $condition = [], array $params = [])
344         {
345                 return self::selectView('post-user-view', $selected, $condition, $params);
346         }
347
348         /**
349          * Select rows from the post-view view
350          *
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 selectPosts(array $selected = [], array $condition = [], array $params = [])
359         {
360                 return self::selectView('post-view', $selected, $condition, $params);
361         }
362
363         /**
364          * Select rows from the post-thread-user-view view
365          *
366          * @param array $selected  Array of selected fields, empty for all
367          * @param array $condition Array of fields for condition
368          * @param array $params    Array of several parameters
369          *
370          * @return boolean|object
371          * @throws \Exception
372          */
373         public static function selectThread(array $selected = [], array $condition = [], array $params = [])
374         {
375                 return self::selectView('post-thread-user-view', $selected, $condition, $params);
376         }
377
378         /**
379          * Select rows from the given view for a given user
380          *
381          * @param string  $view      View (post-user-view or post-thread-user-view)
382          * @param integer $uid       User ID
383          * @param array   $selected  Array of selected fields, empty for all
384          * @param array   $condition Array of fields for condition
385          * @param array   $params    Array of several parameters
386          *
387          * @return boolean|object
388          * @throws \Exception
389          */
390         private static function selectViewForUser(string $view, int $uid, array $selected = [], array $condition = [], array $params = [])
391         {
392                 if (empty($selected)) {
393                         $selected = Item::DISPLAY_FIELDLIST;
394                 }
395
396                 $condition = DBA::mergeConditions($condition,
397                         ["`visible` AND NOT `deleted`
398                         AND NOT `author-blocked` AND NOT `owner-blocked`
399                         AND (NOT `causer-blocked` OR `causer-id` = ? OR `causer-id` IS NULL) AND NOT `contact-blocked`
400                         AND ((NOT `contact-readonly` AND NOT `contact-pending` AND (`contact-rel` IN (?, ?)))
401                                 OR `self` OR `gravity` != ? OR `contact-uid` = ?)
402                         AND NOT `" . $view . "`.`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `uid` = ? AND `hidden`)
403                         AND NOT `author-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `blocked`)
404                         AND NOT `owner-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `blocked`)
405                         AND NOT (`gravity` = ? AND `author-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `ignored`))
406                         AND NOT (`gravity` = ? AND `owner-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `ignored`))",
407                                 0, Contact::SHARING, Contact::FRIEND, Item::GRAVITY_PARENT, 0, $uid, $uid, $uid, Item::GRAVITY_PARENT, $uid, Item::GRAVITY_PARENT, $uid]);
408
409                 $select_string = implode(', ', array_map([DBA::class, 'quoteIdentifier'], $selected));
410
411                 $condition_string = DBA::buildCondition($condition);
412                 $param_string     = DBA::buildParameter($params);
413
414                 $sql = "SELECT " . $select_string . " FROM `" . $view . "` " . $condition_string . $param_string;
415                 $sql = DBA::cleanQuery($sql);
416
417                 return DBA::p($sql, $condition);
418         }
419
420         /**
421          * Select rows from the post-user-view view for a given user
422          *
423          * @param integer $uid       User ID
424          * @param array   $selected  Array of selected fields, empty for all
425          * @param array   $condition Array of fields for condition
426          * @param array   $params    Array of several parameters
427          *
428          * @return boolean|object
429          * @throws \Exception
430          */
431         public static function selectForUser(int $uid, array $selected = [], array $condition = [], array $params = [])
432         {
433                 return self::selectViewForUser('post-user-view', $uid, $selected, $condition, $params);
434         }
435
436         /**
437          * Select rows from the post-view view for a given user
438          *
439          * @param integer $uid       User ID
440          * @param array   $selected  Array of selected fields, empty for all
441          * @param array   $condition Array of fields for condition
442          * @param array   $params    Array of several parameters
443          *
444          * @return boolean|object
445          * @throws \Exception
446          */
447         public static function selectPostsForUser(int $uid, array $selected = [], array $condition = [], array $params = [])
448         {
449                 return self::selectViewForUser('post-view', $uid, $selected, $condition, $params);
450         }
451
452         /**
453          * Select rows from the post-thread-user-view view for a given user
454          *
455          * @param integer $uid       User ID
456          * @param array   $selected  Array of selected fields, empty for all
457          * @param array   $condition Array of fields for condition
458          * @param array   $params    Array of several parameters
459          *
460          * @return boolean|object
461          * @throws \Exception
462          */
463         public static function selectThreadForUser(int $uid, array $selected = [], array $condition = [], array $params = [])
464         {
465                 return self::selectViewForUser('post-thread-user-view', $uid, $selected, $condition, $params);
466         }
467
468         /**
469          * Retrieve a single record from the post-user-view view for a given user and returns it in an associative array
470          *
471          * @param integer $uid User ID
472          * @param array   $selected
473          * @param array   $condition
474          * @param array   $params
475          * @return bool|array
476          * @throws \Exception
477          * @see   DBA::select
478          */
479         public static function selectFirstForUser(int $uid, array $selected = [], array $condition = [], array $params = [])
480         {
481                 $params['limit'] = 1;
482
483                 $result = self::selectForUser($uid, $selected, $condition, $params);
484
485                 if (is_bool($result)) {
486                         return $result;
487                 } else {
488                         $row = self::fetch($result);
489                         DBA::close($result);
490                         return $row;
491                 }
492         }
493
494         /**
495          * Update existing post entries
496          *
497          * @param array $fields    The fields that are to be changed
498          * @param array $condition The condition for finding the item entries
499          *
500          * A return value of "0" doesn't mean an error - but that 0 rows had been changed.
501          *
502          * @return integer|boolean number of affected rows - or "false" if there was an error
503          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
504          */
505         public static function update(array $fields, array $condition)
506         {
507                 $affected = 0;
508
509                 Logger::info('Start Update', ['fields' => $fields, 'condition' => $condition, 'uid' => DI::userSession()->getLocalUserId(),'callstack' => System::callstack(10)]);
510
511                 // Don't allow changes to fields that are responsible for the relation between the records
512                 unset($fields['id']);
513                 unset($fields['parent']);
514                 unset($fields['uid']);
515                 unset($fields['uri']);
516                 unset($fields['uri-id']);
517                 unset($fields['thr-parent']);
518                 unset($fields['thr-parent-id']);
519                 unset($fields['parent-uri']);
520                 unset($fields['parent-uri-id']);
521
522                 $thread_condition = DBA::mergeConditions($condition, ['gravity' => Item::GRAVITY_PARENT]);
523
524                 // To ensure the data integrity we do it in an transaction
525                 DBA::transaction();
526
527                 $update_fields = DI::dbaDefinition()->truncateFieldsForTable('post-user', $fields);
528                 if (!empty($update_fields)) {
529                         $affected_count = 0;
530                         $posts          = DBA::select('post-user-view', ['post-user-id'], $condition);
531                         while ($rows = DBA::toArray($posts, false, 100)) {
532                                 $puids = array_column($rows, 'post-user-id');
533                                 if (!DBA::update('post-user', $update_fields, ['id' => $puids])) {
534                                         DBA::rollback();
535                                         Logger::warning('Updating post-user failed', ['fields' => $update_fields, 'condition' => $condition]);
536                                         return false;
537                                 }
538                                 $affected_count += DBA::affectedRows();
539                         }
540                         DBA::close($posts);
541                         $affected = $affected_count;
542                 }
543
544                 $update_fields = DI::dbaDefinition()->truncateFieldsForTable('post-content', $fields);
545                 if (!empty($update_fields)) {
546                         $affected_count = 0;
547                         $posts          = DBA::select('post-user-view', ['uri-id'], $condition, ['group_by' => ['uri-id']]);
548                         while ($rows = DBA::toArray($posts, false, 100)) {
549                                 $uriids = array_column($rows, 'uri-id');
550                                 if (!DBA::update('post-content', $update_fields, ['uri-id' => $uriids])) {
551                                         DBA::rollback();
552                                         Logger::warning('Updating post-content failed', ['fields' => $update_fields, 'condition' => $condition]);
553                                         return false;
554                                 }
555                                 $affected_count += DBA::affectedRows();
556                         }
557                         DBA::close($posts);
558                         $affected = max($affected, $affected_count);
559                 }
560
561                 $update_fields = DI::dbaDefinition()->truncateFieldsForTable('post', $fields);
562                 if (!empty($update_fields)) {
563                         $affected_count = 0;
564                         $posts          = DBA::select('post-user-view', ['uri-id'], $condition, ['group_by' => ['uri-id']]);
565                         while ($rows = DBA::toArray($posts, false, 100)) {
566                                 $uriids = array_column($rows, 'uri-id');
567
568                                 // Only delete the "post" entry when all "post-user" entries are deleted
569                                 if (!empty($update_fields['deleted']) && DBA::exists('post-user', ['uri-id' => $uriids, 'deleted' => false])) {
570                                         unset($update_fields['deleted']);
571                                 }
572
573                                 if (!DBA::update('post', $update_fields, ['uri-id' => $uriids])) {
574                                         DBA::rollback();
575                                         Logger::warning('Updating post failed', ['fields' => $update_fields, 'condition' => $condition]);
576                                         return false;
577                                 }
578                                 $affected_count += DBA::affectedRows();
579                         }
580                         DBA::close($posts);
581                         $affected = max($affected, $affected_count);
582                 }
583
584                 $update_fields = Post\DeliveryData::extractFields($fields);
585                 if (!empty($update_fields)) {
586                         $affected_count = 0;
587                         $posts          = DBA::select('post-user-view', ['uri-id'], $condition, ['group_by' => ['uri-id']]);
588                         while ($rows = DBA::toArray($posts, false, 100)) {
589                                 $uriids = array_column($rows, 'uri-id');
590                                 if (!DBA::update('post-delivery-data', $update_fields, ['uri-id' => $uriids])) {
591                                         DBA::rollback();
592                                         Logger::warning('Updating post-delivery-data failed', ['fields' => $update_fields, 'condition' => $condition]);
593                                         return false;
594                                 }
595                                 $affected_count += DBA::affectedRows();
596                         }
597                         DBA::close($posts);
598                         $affected = max($affected, $affected_count);
599                 }
600
601                 $update_fields = DI::dbaDefinition()->truncateFieldsForTable('post-thread', $fields);
602                 if (!empty($update_fields)) {
603                         $affected_count = 0;
604                         $posts          = DBA::select('post-user-view', ['uri-id'], $thread_condition, ['group_by' => ['uri-id']]);
605                         while ($rows = DBA::toArray($posts, false, 100)) {
606                                 $uriids = array_column($rows, 'uri-id');
607                                 if (!DBA::update('post-thread', $update_fields, ['uri-id' => $uriids])) {
608                                         DBA::rollback();
609                                         Logger::warning('Updating post-thread failed', ['fields' => $update_fields, 'condition' => $condition]);
610                                         return false;
611                                 }
612                                 $affected_count += DBA::affectedRows();
613                         }
614                         DBA::close($posts);
615                         $affected = max($affected, $affected_count);
616                 }
617
618                 $update_fields = DI::dbaDefinition()->truncateFieldsForTable('post-thread-user', $fields);
619                 if (!empty($update_fields)) {
620                         $affected_count = 0;
621                         $posts          = DBA::select('post-user-view', ['post-user-id'], $thread_condition);
622                         while ($rows = DBA::toArray($posts, false, 100)) {
623                                 $thread_puids = array_column($rows, 'post-user-id');
624                                 if (!DBA::update('post-thread-user', $update_fields, ['post-user-id' => $thread_puids])) {
625                                         DBA::rollback();
626                                         Logger::warning('Updating post-thread-user failed', ['fields' => $update_fields, 'condition' => $condition]);
627                                         return false;
628                                 }
629                                 $affected_count += DBA::affectedRows();
630                         }
631                         DBA::close($posts);
632                         $affected = max($affected, $affected_count);
633                 }
634
635                 DBA::commit();
636
637                 Logger::info('Updated posts', ['rows' => $affected]);
638                 return $affected;
639         }
640
641         /**
642          * Delete a row from the post table
643          *
644          * @param array        $conditions Field condition(s)
645          * @param array        $options
646          *                           - cascade: If true we delete records in other tables that depend on the one we're deleting through
647          *                           relations (default: true)
648          *
649          * @return boolean was the delete successful?
650          * @throws \Exception
651          */
652         public static function delete(array $conditions, array $options = []): bool
653         {
654                 return DBA::delete('post', $conditions, $options);
655         }
656 }