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