diff options
Diffstat (limited to 'include/items.php')
-rw-r--r-- | include/items.php | 1283 |
1 files changed, 960 insertions, 323 deletions
diff --git a/include/items.php b/include/items.php index dc9e9a7d9..b80c5672b 100644 --- a/include/items.php +++ b/include/items.php @@ -240,24 +240,46 @@ function comments_are_now_closed($item) { return false; } -function item_normal() { - $profile_uid = App::$profile['profile_uid'] ?? App::$profile_uid ?? null; +function item_normal($profile_uid = null, $prefix = 'item') { + if ($profile_uid === null) { + $profile_uid = App::$profile['profile_uid'] ?? App::$profile_uid ?? null; + } + $uid = local_channel(); $is_owner = ($uid && intval($profile_uid) === $uid); - $sql = " and item.item_hidden = 0 and item.item_type = 0 and item.item_deleted = 0 - and item.item_unpublished = 0 and item.item_pending_remove = 0"; + $sql = " and $prefix.item_hidden = 0 and $prefix.item_type = 0 and $prefix.item_deleted = 0 + and $prefix.item_unpublished = 0 and $prefix.item_pending_remove = 0"; if ($is_owner) { - $sql .= " and item.item_blocked IN (0, " . intval(ITEM_MODERATED) . ") and item.item_delayed IN (0, 1) "; + $sql .= " and $prefix.item_blocked IN (0, " . intval(ITEM_MODERATED) . ") and $prefix.item_delayed IN (0, 1) "; } else { - $sql .= " and item.item_blocked = 0 and item.item_delayed = 0 "; + $sql .= " and $prefix.item_blocked = 0 and $prefix.item_delayed = 0 "; } return $sql; } +function item_forwardable($item) { + if (intval($item['item_unpublished']) || + intval($item['item_delayed']) || + intval($item['item_blocked']) || + intval($item['item_hidden']) || + intval($item['item_restrict']) || // this might change in the future + // internal follow/unfollow thread + in_array($item['verb'], ['Follow', 'Ignore', ACTIVITY_FOLLOW, ACTIVITY_UNFOLLOW]) || + str_contains($item['postopts'], 'nodeliver') || + // actor not fetchable + (isset($item['author']['xchan_network']) && in_array($item['author']['xchan_network'], ['rss', 'anon', 'token'])) + + ) { + return false; + } + + return true; +} + function item_normal_search() { return " and item.item_hidden = 0 and item.item_type in (0,3,6,7) and item.item_deleted = 0 and item.item_unpublished = 0 and item.item_delayed = 0 and item.item_pending_remove = 0 @@ -361,7 +383,7 @@ function can_comment_on_post($observer_xchan, $item) { case 'specific': case 'contacts': case '': - if(local_channel() && get_abconfig(local_channel(), (($item['verb'] === ACTIVITY_SHARE) ? $item['source_xchan'] : $item['owner_xchan']), 'their_perms', 'post_comments')) { + if(local_channel() && get_abconfig(local_channel(), $item['owner_xchan'], 'their_perms', 'post_comments')) { return true; } if(intval($item['item_wall']) && perm_is_allowed($item['uid'],$observer_xchan,'post_comments')) { @@ -431,12 +453,13 @@ function add_source_route($iid, $hash) { * * \e boolean \b success true or false * * \e array \b activity the resulting activity if successful */ -function post_activity_item($arr, $allow_code = false, $deliver = true) { + +function post_activity_item($arr, $allow_code = false, $deliver = true, $channel = null, $observer = null, $addAndSync = true) { $ret = array('success' => false); $is_comment = false; - if((($arr['parent']) && $arr['parent'] != $arr['id']) || (($arr['parent_mid']) && $arr['parent_mid'] != $arr['mid'])) + if((isset($arr['parent'], $arr['id']) && intval($arr['parent']) !== intval($arr['id'])) || (isset($arr['parent_mid'], $arr['mid']) && $arr['parent_mid'] !== $arr['mid'])) $is_comment = true; if(! array_key_exists('item_origin',$arr)) @@ -446,11 +469,16 @@ function post_activity_item($arr, $allow_code = false, $deliver = true) { if(! array_key_exists('item_thread_top',$arr) && (! $is_comment)) $arr['item_thread_top'] = 1; - $channel = App::get_channel(); - $observer = App::get_observer(); + if (!$channel) { + $channel = App::get_channel(); + } + + if (!$observer) { + $observer = App::get_observer(); + } - $arr['aid'] = ((x($arr,'aid')) ? $arr['aid'] : $channel['channel_account_id']); - $arr['uid'] = ((x($arr,'uid')) ? $arr['uid'] : $channel['channel_id']); + $arr['aid'] = ((!empty($arr['aid'])) ? $arr['aid'] : $channel['channel_account_id']); + $arr['uid'] = ((!empty($arr['uid'])) ? $arr['uid'] : $channel['channel_id']); if(! perm_is_allowed($arr['uid'],$observer['xchan_hash'],(($is_comment) ? 'post_comments' : 'post_wall'))) { $ret['message'] = t('Permission denied'); @@ -466,18 +494,18 @@ function post_activity_item($arr, $allow_code = false, $deliver = true) { $arr['mimetype'] = 'text/bbcode'; - if(! $arr['mid']) { - $arr['uuid'] = ((x($arr,'uuid')) ? $arr['uuid'] : new_uuid()); + if(empty($arr['mid'])) { + $arr['uuid'] = ((!empty($arr['uuid'])) ? $arr['uuid'] : new_uuid()); } - $arr['mid'] = ((x($arr,'mid')) ? $arr['mid'] : z_root() . '/item/' . $arr['uuid']); - $arr['parent_mid'] = ((x($arr,'parent_mid')) ? $arr['parent_mid'] : $arr['mid']); - $arr['thr_parent'] = ((x($arr,'thr_parent')) ? $arr['thr_parent'] : $arr['mid']); + $arr['mid'] = ((!empty($arr['mid'])) ? $arr['mid'] : z_root() . '/item/' . $arr['uuid']); + $arr['parent_mid'] = ((!empty($arr['parent_mid'])) ? $arr['parent_mid'] : $arr['mid']); + $arr['thr_parent'] = ((!empty($arr['thr_parent'])) ? $arr['thr_parent'] : $arr['mid']); - $arr['owner_xchan'] = ((x($arr,'owner_xchan')) ? $arr['owner_xchan'] : $channel['channel_hash']); - $arr['author_xchan'] = ((x($arr,'author_xchan')) ? $arr['author_xchan'] : $observer['xchan_hash']); + $arr['owner_xchan'] = ((!empty($arr['owner_xchan'])) ? $arr['owner_xchan'] : $channel['channel_hash']); + $arr['author_xchan'] = ((!empty($arr['author_xchan'])) ? $arr['author_xchan'] : $observer['xchan_hash']); - $arr['verb'] = ((x($arr,'verb')) ? $arr['verb'] : 'Create'); - $arr['obj_type'] = ((x($arr,'obj_type')) ? $arr['obj_type'] : 'Note'); + $arr['verb'] = ((!empty($arr['verb'])) ? $arr['verb'] : 'Create'); + $arr['obj_type'] = ((!empty($arr['obj_type'])) ? $arr['obj_type'] : 'Note'); if(! ( array_key_exists('allow_cid',$arr) || array_key_exists('allow_gid',$arr) || array_key_exists('deny_cid',$arr) || array_key_exists('deny_gid',$arr))) { @@ -489,10 +517,18 @@ function post_activity_item($arr, $allow_code = false, $deliver = true) { $arr['comment_policy'] = map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments')); - if ((! $arr['plink']) && (intval($arr['item_thread_top']))) { + if (empty($arr['plink']) && (intval($arr['item_thread_top']))) { $arr['plink'] = $arr['mid']; } + if (!$arr['target']) { + $arr['target'] = [ + 'id' => str_replace('/item/', '/conversation/', $arr['parent_mid']), + 'type' => 'Collection', + 'attributedTo' => z_root() . '/channel/' . $channel['channel_address'], + ]; + $arr['tgt_type'] = 'Collection'; + } // for the benefit of plugins, we will behave as if this is an API call rather than a normal online post @@ -504,34 +540,35 @@ function post_activity_item($arr, $allow_code = false, $deliver = true) { */ call_hooks('post_local', $arr); - if(x($arr, 'cancel')) { + if (!empty($arr['cancel'])) { logger('Post cancelled by plugin.'); return $ret; } - $post = item_store($arr,$allow_code,$deliver); - - if($post['success']) { - $post_id = $post['item_id']; - $ret['success'] = true; - $ret['item_id'] = $post_id; - $ret['activity'] = $post['item']; + $post = item_store($arr, $allow_code, $deliver, $addAndSync); - /** - * @hooks post_local_end - * Called after a local post operation has completed. - * * \e array - the item returned from item_store() - */ - call_hooks('post_local_end', $ret['activity']); - } - else + if (!$post['success']) { return $ret; - - if($post_id && $deliver) { - Master::Summon(['Notifier','activity', $post_id]); } + $post_id = $post['item_id']; $ret['success'] = true; + $ret['item_id'] = $post_id; + $ret['activity'] = $post['item']; + + /** + * @hooks post_local_end + * Called after a local post operation has completed. + * * \e array - the item returned from item_store() + */ + call_hooks('post_local_end', $ret['activity']); + + if($post_id && $deliver) { + Master::Summon(['Notifier', 'activity', $post_id]); + if (!empty($post['approval_id'])) { + Master::Summon(['Notifier', 'activity', $post['approval_id']]); + } + } return $ret; } @@ -693,14 +730,14 @@ function get_item_elements($x,$allow_code = false) { if($arr['edited'] > datetime_convert()) $arr['edited'] = datetime_convert(); - $arr['expires'] = ((x($x,'expires') && $x['expires']) + $arr['expires'] = ((!empty($x['expires']) && $x['expires']) ? datetime_convert('UTC','UTC',$x['expires']) : NULL_DATE); - $arr['commented'] = ((x($x,'commented') && $x['commented']) + $arr['commented'] = ((!empty($x['commented']) && $x['commented']) ? datetime_convert('UTC','UTC',$x['commented']) : $arr['created']); - $arr['comments_closed'] = ((x($x,'comments_closed') && $x['comments_closed']) + $arr['comments_closed'] = ((!empty($x['comments_closed']) && $x['comments_closed']) ? datetime_convert('UTC','UTC',$x['comments_closed']) : NULL_DATE); @@ -1594,7 +1631,7 @@ function item_json_encapsulate($arr, $k) { * * \e boolean \b success * * \e int \b item_id */ -function item_store($arr, $allow_exec = false, $deliver = true) { +function item_store($arr, $allow_exec = false, $deliver = true, $addAndSync = true) { $d = [ 'item' => $arr, @@ -1645,7 +1682,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { if(array_key_exists('parent',$arr)) unset($arr['parent']); - $arr['mimetype'] = ((x($arr,'mimetype')) ? notags(trim($arr['mimetype'])) : 'text/bbcode'); + $arr['mimetype'] = ((!empty($arr['mimetype'])) ? notags(trim($arr['mimetype'])) : 'text/bbcode'); if(($arr['mimetype'] == 'application/x-php') && (! $allow_exec)) { logger('item_store: php mimetype but allow_exec is denied.'); @@ -1657,19 +1694,19 @@ function item_store($arr, $allow_exec = false, $deliver = true) { $arr['summary'] = ((array_key_exists('summary',$arr) && $arr['summary']) ? trim($arr['summary']) : ''); $arr['body'] = ((array_key_exists('body',$arr) && $arr['body']) ? trim($arr['body']) : ''); - $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : ''); - $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : ''); - $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : ''); - $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : ''); - $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : ''); - $arr['route'] = ((x($arr,'route')) ? trim($arr['route']) : ''); - $arr['uuid'] = ((x($arr,'uuid')) ? trim($arr['uuid']) : ''); - $arr['item_private'] = ((x($arr,'item_private')) ? intval($arr['item_private']) : 0 ); - $arr['item_wall'] = ((x($arr,'item_wall')) ? intval($arr['item_wall']) : 0 ); - $arr['item_type'] = ((x($arr,'item_type')) ? intval($arr['item_type']) : 0 ); + $arr['allow_cid'] = ((!empty($arr['allow_cid'])) ? trim($arr['allow_cid']) : ''); + $arr['allow_gid'] = ((!empty($arr['allow_gid'])) ? trim($arr['allow_gid']) : ''); + $arr['deny_cid'] = ((!empty($arr['deny_cid'])) ? trim($arr['deny_cid']) : ''); + $arr['deny_gid'] = ((!empty($arr['deny_gid'])) ? trim($arr['deny_gid']) : ''); + $arr['postopts'] = ((!empty($arr['postopts'])) ? trim($arr['postopts']) : ''); + $arr['route'] = ((!empty($arr['route'])) ? trim($arr['route']) : ''); + $arr['uuid'] = ((!empty($arr['uuid'])) ? trim($arr['uuid']) : ''); + $arr['item_private'] = ((!empty($arr['item_private'])) ? intval($arr['item_private']) : 0 ); + $arr['item_wall'] = ((!empty($arr['item_wall'])) ? intval($arr['item_wall']) : 0 ); + $arr['item_type'] = ((!empty($arr['item_type'])) ? intval($arr['item_type']) : 0 ); // obsolete, but needed so as not to throw not-null constraints on some database driveres - $arr['item_flags'] = ((x($arr,'item_flags')) ? intval($arr['item_flags']) : 0 ); + $arr['item_flags'] = ((!empty($arr['item_flags'])) ? intval($arr['item_flags']) : 0 ); $arr['lang'] = detect_language($arr['body']); @@ -1710,32 +1747,32 @@ function item_store($arr, $allow_exec = false, $deliver = true) { $arr = $translate['item']; } - if((x($arr,'obj')) && is_array($arr['obj'])) { + if((!empty($arr['obj'])) && is_array($arr['obj'])) { activity_sanitise($arr['obj']); $arr['obj'] = json_encode($arr['obj']); } - if((x($arr,'target')) && is_array($arr['target'])) { + if((!empty($arr['target'])) && is_array($arr['target'])) { activity_sanitise($arr['target']); $arr['target'] = json_encode($arr['target']); } - if((x($arr,'attach')) && is_array($arr['attach'])) { + if((!empty($arr['attach'])) && is_array($arr['attach'])) { activity_sanitise($arr['attach']); $arr['attach'] = json_encode($arr['attach']); } - $arr['aid'] = ((x($arr,'aid')) ? intval($arr['aid']) : 0); - $arr['mid'] = ((x($arr,'mid')) ? notags(trim($arr['mid'])) : random_string()); - $arr['revision'] = ((x($arr,'revision') && intval($arr['revision']) > 0) ? intval($arr['revision']) : 0); + $arr['aid'] = ((!empty($arr['aid'])) ? intval($arr['aid']) : 0); + $arr['mid'] = ((!empty($arr['mid'])) ? notags(trim($arr['mid'])) : random_string()); + $arr['revision'] = ((!empty($arr['revision']) && intval($arr['revision']) > 0) ? intval($arr['revision']) : 0); - $arr['author_xchan'] = ((x($arr,'author_xchan')) ? notags(trim($arr['author_xchan'])) : ''); - $arr['owner_xchan'] = ((x($arr,'owner_xchan')) ? notags(trim($arr['owner_xchan'])) : ''); - $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert()); - $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert()); - $arr['expires'] = ((x($arr,'expires') !== false) ? datetime_convert('UTC','UTC',$arr['expires']) : NULL_DATE); - $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert()); - $arr['comments_closed'] = ((x($arr,'comments_closed') !== false) ? datetime_convert('UTC','UTC',$arr['comments_closed']) : NULL_DATE); + $arr['author_xchan'] = ((!empty($arr['author_xchan'])) ? notags(trim($arr['author_xchan'])) : ''); + $arr['owner_xchan'] = ((!empty($arr['owner_xchan'])) ? notags(trim($arr['owner_xchan'])) : ''); + $arr['created'] = ((!empty($arr['created']) !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert()); + $arr['edited'] = ((!empty($arr['edited']) !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert()); + $arr['expires'] = ((!empty($arr['expires']) !== false) ? datetime_convert('UTC','UTC',$arr['expires']) : NULL_DATE); + $arr['commented'] = ((!empty($arr['commented']) !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert()); + $arr['comments_closed'] = ((!empty($arr['comments_closed']) !== false) ? datetime_convert('UTC','UTC',$arr['comments_closed']) : NULL_DATE); $arr['html'] = ((array_key_exists('html',$arr)) ? $arr['html'] : ''); if($deliver) { @@ -1749,26 +1786,26 @@ function item_store($arr, $allow_exec = false, $deliver = true) { // will still take place through backdoor methods. Since these fields are rarely used // otherwise, just preserve the original timestamp. - $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert()); - $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert()); + $arr['received'] = ((!empty($arr['received']) !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert()); + $arr['changed'] = ((!empty($arr['changed']) !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert()); } - $arr['location'] = ((x($arr,'location')) ? notags(trim($arr['location'])) : ''); - $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : ''); - $arr['parent_mid'] = ((x($arr,'parent_mid')) ? notags(trim($arr['parent_mid'])) : ''); - $arr['thr_parent'] = ((x($arr,'thr_parent')) ? notags(trim($arr['thr_parent'])) : $arr['parent_mid']); - $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : 'Create'); - $arr['obj_type'] = ((x($arr,'obj_type')) ? notags(trim($arr['obj_type'])) : 'Note'); - $arr['obj'] = ((x($arr,'obj')) ? trim($arr['obj']) : ''); - $arr['tgt_type'] = ((x($arr,'tgt_type')) ? notags(trim($arr['tgt_type'])) : ''); - $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : ''); - $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : ''); - $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : ''); - $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : ''); + $arr['location'] = ((!empty($arr['location'])) ? notags(trim($arr['location'])) : ''); + $arr['coord'] = ((!empty($arr['coord'])) ? notags(trim($arr['coord'])) : ''); + $arr['parent_mid'] = ((!empty($arr['parent_mid'])) ? notags(trim($arr['parent_mid'])) : ''); + $arr['thr_parent'] = ((!empty($arr['thr_parent'])) ? notags(trim($arr['thr_parent'])) : $arr['parent_mid']); + $arr['verb'] = ((!empty($arr['verb'])) ? notags(trim($arr['verb'])) : 'Create'); + $arr['obj_type'] = ((!empty($arr['obj_type'])) ? notags(trim($arr['obj_type'])) : 'Note'); + $arr['obj'] = ((!empty($arr['obj'])) ? trim($arr['obj']) : ''); + $arr['tgt_type'] = ((!empty($arr['tgt_type'])) ? notags(trim($arr['tgt_type'])) : ''); + $arr['target'] = ((!empty($arr['target'])) ? trim($arr['target']) : ''); + $arr['plink'] = ((!empty($arr['plink'])) ? notags(trim($arr['plink'])) : ''); + $arr['attach'] = ((!empty($arr['attach'])) ? notags(trim($arr['attach'])) : ''); + $arr['app'] = ((!empty($arr['app'])) ? notags(trim($arr['app'])) : ''); - $arr['public_policy'] = ((x($arr,'public_policy')) ? notags(trim($arr['public_policy'])) : '' ); + $arr['public_policy'] = ((!empty($arr['public_policy'])) ? notags(trim($arr['public_policy'])) : '' ); - $arr['comment_policy'] = ((x($arr,'comment_policy')) ? notags(trim($arr['comment_policy'])) : 'contacts' ); + $arr['comment_policy'] = ((!empty($arr['comment_policy'])) ? notags(trim($arr['comment_policy'])) : 'contacts' ); if(! array_key_exists('item_unseen',$arr)) $arr['item_unseen'] = 1; @@ -1776,16 +1813,6 @@ function item_store($arr, $allow_exec = false, $deliver = true) { if((! array_key_exists('item_nocomment',$arr)) && ($arr['comment_policy'] == 'none')) $arr['item_nocomment'] = 1; - // handle time travelers - // Allow a bit of fudge in case somebody just has a slightly slow/fast clock - - $d1 = new DateTime('now +10 minutes', new DateTimeZone('UTC')); - $d2 = new DateTime($arr['created'] . '+00:00'); - - if($d2 > $d1) { - $arr['item_delayed'] = 1; - } - if(empty($arr['llink'])) { $arr['llink'] = z_root() . '/display/' . $arr['uuid']; } @@ -1827,7 +1854,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { ); } - if(comments_are_now_closed($r[0])) { + if(comments_are_now_closed($r[0]) && !in_array($arr['verb'], ['Add', 'Remove'])) { logger('item_store: comments closed'); $ret['message'] = 'Comments closed.'; return $ret; @@ -1882,7 +1909,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { $arr['item_private'] = 0; if(in_array($arr['obj_type'], ['Note','Answer']) && $r[0]['obj_type'] === 'Question' && intval($r[0]['item_wall'])) { - Activity::update_poll($r[0]['id'], $arr); + Activity::update_poll($r[0], $arr); } } @@ -1922,7 +1949,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { */ call_hooks('post_remote', $arr); - if(x($arr, 'cancel')) { + if(!empty($arr['cancel'])) { logger('Post cancelled by plugin.'); $ret['message'] = 'cancelled.'; return $ret; @@ -1965,7 +1992,9 @@ function item_store($arr, $allow_exec = false, $deliver = true) { // find the item we just created - $r = q("SELECT * FROM item WHERE mid = '%s' AND uid = %d and revision = %d ORDER BY id ASC ", + $r = q("SELECT item.*, tp.uuid AS thr_parent_uuid FROM item + LEFT JOIN item tp ON item.thr_parent = tp.mid AND item.uid = tp.uid + WHERE item.mid = '%s' AND item.uid = %d and item.revision = %d ORDER BY item.id ASC ", dbesc($arr['mid']), intval($arr['uid']), intval($arr['revision']) @@ -2047,9 +2076,15 @@ function item_store($arr, $allow_exec = false, $deliver = true) { item_update_parent_commented($arr); + if (str_contains($arr['body'], '[/embed]') || str_contains($arr['body'], '[/img]') || str_contains($arr['body'], '[/zmg]')) { + Master::Summon(['Cache_embeds', $arr['uuid']]); + } - if(strpos($arr['body'],'[embed]') !== false) { - Master::Summon([ 'Cache_embeds', $current_post ]); + $ret['success'] = true; + $ret['item_id'] = $current_post; + + if ($addAndSync) { + $ret = addToCollectionAndSync($ret); } // If _creating_ a deleted item, don't propagate it further or send out notifications. @@ -2062,9 +2097,6 @@ function item_store($arr, $allow_exec = false, $deliver = true) { tag_deliver($arr['uid'],$current_post); } - $ret['success'] = true; - $ret['item_id'] = $current_post; - return $ret; } @@ -2077,7 +2109,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { * @param boolean $deliver (optional) default true * @return array */ -function item_store_update($arr, $allow_exec = false, $deliver = true) { +function item_store_update($arr, $allow_exec = false, $deliver = true, $addAndSync = true) { $d = [ 'item' => $arr, @@ -2135,7 +2167,7 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { if(array_key_exists('edit',$arr)) unset($arr['edit']); - $arr['mimetype'] = ((x($arr,'mimetype')) ? notags(trim($arr['mimetype'])) : 'text/bbcode'); + $arr['mimetype'] = ((!empty($arr['mimetype'])) ? notags(trim($arr['mimetype'])) : 'text/bbcode'); if(($arr['mimetype'] == 'application/x-php') && (! $allow_exec)) { logger('item_store: php mimetype but allow_exec is denied.'); @@ -2206,12 +2238,12 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { unset($arr['thr_parent']); unset($arr['llink']); - $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert()); - $arr['expires'] = ((x($arr,'expires') !== false) ? datetime_convert('UTC','UTC',$arr['expires']) : $orig[0]['expires']); + $arr['edited'] = ((!empty($arr['edited'])) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert()); + $arr['expires'] = ((!empty($arr['expires'])) ? datetime_convert('UTC','UTC',$arr['expires']) : $orig[0]['expires']); - $arr['revision'] = ((x($arr,'revision') && $arr['revision'] > 0) ? intval($arr['revision']) : 0); + $arr['revision'] = ((!empty($arr['revision'])) ? intval($arr['revision']) : 0); - if(array_key_exists('comments_closed',$arr) && $arr['comments_closed'] > NULL_DATE) + if(array_key_exists('comments_closed',$arr)) $arr['comments_closed'] = datetime_convert('UTC','UTC',$arr['comments_closed']); else $arr['comments_closed'] = $orig[0]['comments_closed']; @@ -2223,15 +2255,15 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { $arr['route'] = ((array_key_exists('route',$arr)) ? trim($arr['route']) : $orig[0]['route']); - $arr['location'] = ((x($arr,'location')) ? notags(trim($arr['location'])) : $orig[0]['location']); - $arr['uuid'] = ((x($arr,'uuid')) ? notags(trim($arr['uuid'])) : $orig[0]['uuid']); - $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : $orig[0]['coord']); - $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : $orig[0]['verb']); - $arr['obj_type'] = ((x($arr,'obj_type')) ? notags(trim($arr['obj_type'])) : $orig[0]['obj_type']); - $arr['obj'] = ((x($arr,'obj')) ? trim($arr['obj']) : $orig[0]['obj']); - $arr['tgt_type'] = ((x($arr,'tgt_type')) ? notags(trim($arr['tgt_type'])) : $orig[0]['tgt_type']); - $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : $orig[0]['target']); - $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : $orig[0]['plink']); + $arr['location'] = ((!empty($arr['location'])) ? notags(trim($arr['location'])) : $orig[0]['location']); + $arr['uuid'] = ((!empty($arr['uuid'])) ? notags(trim($arr['uuid'])) : $orig[0]['uuid']); + $arr['coord'] = ((!empty($arr['coord'])) ? notags(trim($arr['coord'])) : $orig[0]['coord']); + $arr['verb'] = ((!empty($arr['verb'])) ? notags(trim($arr['verb'])) : $orig[0]['verb']); + $arr['obj_type'] = ((!empty($arr['obj_type'])) ? notags(trim($arr['obj_type'])) : $orig[0]['obj_type']); + $arr['obj'] = ((!empty($arr['obj'])) ? trim($arr['obj']) : $orig[0]['obj']); + $arr['tgt_type'] = ((!empty($arr['tgt_type'])) ? notags(trim($arr['tgt_type'])) : $orig[0]['tgt_type']); + $arr['target'] = ((!empty($arr['target'])) ? trim($arr['target']) : $orig[0]['target']); + $arr['plink'] = ((!empty($arr['plink'])) ? notags(trim($arr['plink'])) : $orig[0]['plink']); $arr['allow_cid'] = ((array_key_exists('allow_cid',$arr)) ? trim($arr['allow_cid']) : $orig[0]['allow_cid']); $arr['allow_gid'] = ((array_key_exists('allow_gid',$arr)) ? trim($arr['allow_gid']) : $orig[0]['allow_gid']); @@ -2270,11 +2302,11 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { $arr['item_pending_remove'] = ((array_key_exists('item_pending_remove',$arr)) ? intval($arr['item_pending_remove']) : $orig[0]['item_pending_remove'] ); $arr['item_blocked'] = ((array_key_exists('item_blocked',$arr)) ? intval($arr['item_blocked']) : $orig[0]['item_blocked'] ); - $arr['sig'] = ((x($arr,'sig')) ? $arr['sig'] : ''); + $arr['sig'] = ((!empty($arr['sig'])) ? $arr['sig'] : ''); $arr['layout_mid'] = ((array_key_exists('layout_mid',$arr)) ? dbesc($arr['layout_mid']) : $orig[0]['layout_mid'] ); - $arr['public_policy'] = ((x($arr,'public_policy')) ? notags(trim($arr['public_policy'])) : $orig[0]['public_policy'] ); - $arr['comment_policy'] = ((x($arr,'comment_policy')) ? notags(trim($arr['comment_policy'])) : $orig[0]['comment_policy'] ); + $arr['public_policy'] = ((!empty($arr['public_policy'])) ? notags(trim($arr['public_policy'])) : $orig[0]['public_policy'] ); + $arr['comment_policy'] = ((!empty($arr['comment_policy'])) ? notags(trim($arr['comment_policy'])) : $orig[0]['comment_policy'] ); /** * @hooks post_remote_update @@ -2282,7 +2314,7 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { */ call_hooks('post_remote_update', $arr); - if(x($arr, 'cancel')) { + if(!empty($arr['cancel'])) { logger('Post cancelled by plugin.'); $ret['message'] = 'cancelled.'; return $ret; @@ -2334,7 +2366,9 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { // fetch an unescaped complete copy of the stored item - $r = q("select * from item where id = %d", + $r = q("SELECT item.*, tp.uuid AS thr_parent_uuid FROM item + LEFT JOIN item tp ON item.thr_parent = tp.mid AND item.uid = tp.uid + WHERE item.id = %d", intval($orig_post_id) ); if($r) @@ -2348,14 +2382,15 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { if(is_array($terms)) { foreach($terms as $t) { - q("insert into term (uid,oid,otype,ttype,term,url) - values(%d,%d,%d,%d,'%s','%s') ", + q("insert into term (uid, oid, otype, ttype, term, url, imgurl) + values (%d, %d, %d, %d, '%s', '%s', '%s')", intval($uid), intval($orig_post_id), intval(TERM_OBJ_POST), intval($t['ttype']), dbesc($t['term']), - dbesc($t['url']) + dbesc($t['url']), + dbesc($t['imgurl'] ?? ''), ); } $arr['term'] = $terms; @@ -2386,21 +2421,23 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { */ call_hooks('item_stored_update',$arr); - if(strpos($arr['body'],'[embed]') !== false) { - Master::Summon([ 'Cache_embeds', $orig_post_id ]); + if (str_contains($arr['body'], '[/embed]') || str_contains($arr['body'], '[/img]') || str_contains($arr['body'], '[/zmg]')) { + Master::Summon(['Cache_embeds', $arr['uuid']]); } + $ret['success'] = true; + $ret['item_id'] = $orig_post_id; - + if ($addAndSync) { + $ret = addToCollectionAndSync($ret); + } if($deliver) { - send_status_notifications($orig_post_id,$arr); + // don't send notify_comment for edits + // send_status_notifications($orig_post_id,$arr); tag_deliver($uid,$orig_post_id); } - $ret['success'] = true; - $ret['item_id'] = $orig_post_id; - return $ret; } @@ -2448,21 +2485,23 @@ function send_status_notifications($post_id,$item) { $parent = 0; $is_reaction = false; - $thr_parent_id = 0; + $thr_parent_id = null; + $thr_parent_uuid = null; $type = ((intval($item['item_private']) === 2) ? NOTIFY_MAIL : NOTIFY_COMMENT); - if(array_key_exists('verb',$item) && activity_match($item['verb'], ['Like', 'Dislike', ACTIVITY_LIKE, ACTIVITY_DISLIKE])) { + if(array_key_exists('verb',$item) && activity_match($item['verb'], ['Like', 'Dislike', ACTIVITY_LIKE, ACTIVITY_DISLIKE, 'Announce'])) { $type = NOTIFY_LIKE; - $r = q("select id from item where mid = '%s' and uid = %d limit 1", + $r = q("select id, uuid from item where mid = '%s' and uid = %d limit 1", dbesc($item['thr_parent']), intval($item['uid']) ); if ($r) { $thr_parent_id = $r[0]['id']; + $thr_parent_uuid = $r[0]['uuid']; } } @@ -2487,6 +2526,7 @@ function send_status_notifications($post_id,$item) { dbesc($item['parent_mid']), intval($item['uid']) ); + if($x) { foreach($x as $xx) { if($xx['author_xchan'] === $r[0]['channel_hash']) { @@ -2531,7 +2571,7 @@ function send_status_notifications($post_id,$item) { 'link' => $link, 'verb' => $item['verb'], 'otype' => 'item', - 'parent' => $thr_parent_id ? $thr_parent_id : $parent, + 'parent' => $thr_parent_id ?? $parent, 'parent_mid' => $thr_parent_id ? $item['thr_parent'] : $item['parent_mid'] )); } @@ -2808,8 +2848,8 @@ function tag_deliver($uid, $item_id) { 'from_xchan' => $item['author_xchan'], 'type' => NOTIFY_TAGSELF, 'item' => $item, - 'link' => $i[0]['llink'], - 'verb' => ACTIVITY_TAG, + 'link' => $item['llink'], + 'verb' => $item['verb'], 'otype' => 'item' )); @@ -2951,35 +2991,32 @@ function tgroup_check($uid, $item) { // post to group via DM if ($is_group) { - if (intval($item['item_private']) === 2 && $item['mid'] === $item['parent_mid']) { + if (intval($item['item_private']) === 2 && $item['mid'] === $item['parent_mid'] && perm_is_allowed($uid, $item['owner_xchan'], 'post_wall')) { return true; } } - // see if we already have this item. Maybe it is being updated. $r = q("select id from item where mid = '%s' and uid = %d limit 1", dbesc($item['mid']), intval($uid) ); - if($r) + + if ($r) { return true; + } - if(! perm_is_allowed($uid,$item['author_xchan'],'tag_deliver')) - return false; + $u = channelx_by_n($uid); - $u = q("select * from channel left join xchan on channel_hash = xchan_hash where channel_id = %d limit 1", - intval($uid) - ); - - if(! $u) + if (!$u) { return false; + } $max_forums = Config::Get('system','max_tagged_forums',2); $matched_forums = 0; - $link = normalise_link($u[0]['xchan_url']); + $link = normalise_link($u['xchan_url']); $terms = []; @@ -2996,7 +3033,7 @@ function tgroup_check($uid, $item) { } $mention = true; - logger('tgroup_check: mention found for ' . $u[0]['channel_name']); + logger('tgroup_check: mention found for ' . $u['channel_name']); // At this point we've determined that the person receiving this post was mentioned in it. // Now let's check if this mention was inside a reshare so we don't spam a forum @@ -3104,6 +3141,11 @@ function i_am_mentioned($channel, $item, $check_groups = false) { */ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false, $edit = false) { + if ($item['author_xchan'] === $channel['channel_hash'] && in_array($item['verb'], ['Add', 'Remove'])) { + logger('delivery chain already started'); + return; + } + $sourced = check_item_source($channel['channel_id'],$item); if($sourced) { @@ -3149,17 +3191,113 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false $item['parent_mid'] = $item['mid']; $item['thr_parent'] = $item['mid']; $item['llink'] = z_root() . '/display/' . $item['uuid']; + $item['target'] = json_encode([ + 'id' => str_replace('/item/', '/conversation/', $item['mid']), + 'type' => 'Collection', + 'attributedTo' => z_root() . '/channel/' . $channel['channel_address'] + ]); + $item['tgt_type'] = 'Collection'; } + } - $r = q("UPDATE item SET author_xchan = '%s', mid = '%s', parent_mid = '%s', thr_parent = '%s', llink = '%s' WHERE id = %d", - dbesc($item['author_xchan']), - dbesc($item['mid']), - dbesc($item['parent_mid']), - dbesc($item['thr_parent']), - dbesc($item['llink']), + $private = (($channel['channel_allow_cid'] || $channel['channel_allow_gid'] + || $channel['channel_deny_cid'] || $channel['channel_deny_gid']) ? 1 : 0); + + $new_public_policy = map_scope(PermissionLimits::Get($channel['channel_id'],'view_stream'),true); + + if((! $private) && $new_public_policy) + $private = 1; + + $item_wall = 1; + $item_origin = (($item['item_deleted']) ? 0 : 1); // item_origin for deleted items is set to 0 in delete_imported_item() to prevent looping. In this case we probably should not set it back to 1 here. + $item_uplink = 0; + $item_nocomment = 0; + + $flag_bits = $item['item_flags']; + + // maintain the original source, which will be the original item owner and was stored in source_xchan + // when we created the delivery fork + + if($parent) { + $r = q("update item set source_xchan = '%s' where id = %d", + dbesc($parent['source_xchan']), intval($item_id) ); } + else { + $item_uplink = (($item['item_rss']) ? 0 : 1); // Do not set item_uplink for rss items - we can not send anything to them. + + // if this is an edit, item_store_update() will have already updated the item + // with the correct value for source_xchan (by ignoring it). We cannot set to owner_xchan + // in this case because owner_xchan will point to the parent of this chain + // and not the original sender. + + if(!$edit) { + $r = q("update item set source_xchan = owner_xchan where id = %d", + intval($item_id) + ); + } + } + + // this will not work with item_store_update() + + $r = q("update item set item_uplink = %d, item_nocomment = %d, item_flags = %d, owner_xchan = '%s', allow_cid = '%s', allow_gid = '%s', + deny_cid = '%s', deny_gid = '%s', item_private = %d, public_policy = '%s', comment_policy = '%s', title = '%s', body = '%s', item_wall = %d, item_origin = %d, + author_xchan = '%s', mid = '%s', parent_mid = '%s', thr_parent = '%s', llink = '%s', target = '%s', tgt_type = '%s' where id = %d", + intval($item_uplink), + intval($item_nocomment), + intval($flag_bits), + dbesc($channel['channel_hash']), + dbesc($channel['channel_allow_cid']), + dbesc($channel['channel_allow_gid']), + dbesc($channel['channel_deny_cid']), + dbesc($channel['channel_deny_gid']), + intval($private), + dbesc($new_public_policy), + dbesc(map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments'))), + dbesc($item['title']), + dbesc($item['body']), + intval($item_wall), + intval($item_origin), + dbesc($item['author_xchan']), + dbesc($item['mid']), + dbesc($item['parent_mid']), + dbesc($item['thr_parent']), + dbesc($item['llink']), + dbesc($item['target']), + dbesc($item['tgt_type']), + intval($item_id) + ); + + if($r) { + $rr = q("select * from item where id = %d", + intval($item_id) + ); + + if ($rr) { + + // this is hackish but since we can not use item_store_update() here, + // we will prepare a similar output to feed to addToCollectionAndSync() + $ret['success'] = 1; + $ret['item_id'] = $rr[0]['id']; + $ret['item'] = $rr[0]; + + $result = addToCollectionAndSync($ret); + + Master::Summon(['Notifier', 'tgroup', $result['item_id']]); + if ($result['approval_id']) { + Master::Summon(['Notifier', 'tgroup', $result['approval_id']]); + } + } + } + else { + logger('start_delivery_chain: failed to update item'); + // reset the source xchan to prevent loops + $r = q("update item set source_xchan = '' where id = %d", + intval($item_id) + ); + } + return; } if ($group && (! $parent)) { @@ -3181,8 +3319,8 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false if ($r) { if (intval($item['item_deleted'])) { - drop_item($r[0]['id'], false, DROPITEM_PHASE1); - Master::Summon([ 'Notifier', 'drop', $r[0]['id'] ]); + drop_item($r[0]['id'], DROPITEM_PHASE1, uid: $r[0]['uid']); + Master::Summon(['Notifier', 'drop' ,$r[0]['id']]); return; } $arr['id'] = intval($r[0]['id']); @@ -3199,9 +3337,14 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false } else { - $arr['uuid'] = item_message_id(); + // To prevent duplicates from possible clones of the forum/group, + // we will create a v5 UUID of the source item mid. + // Add some extra entropy to prevent duplicate UUIDs with items where we already + // created an UUID from the mid (activities which do not provide an UUID field). + $arr['uuid'] = uuid_from_url($item['mid'] . '#group_item'); $arr['mid'] = z_root() . '/item/' . $arr['uuid']; $arr['parent_mid'] = $arr['mid']; + $arr['plink'] = $arr['mid']; } $arr['aid'] = $channel['channel_account_id']; @@ -3262,8 +3405,15 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false } $arr['title'] = $item['title']; - $arr['tgt_type'] = $item['tgt_type']; - $arr['target'] = $item['target']; +// $arr['tgt_type'] = $item['tgt_type']; +// $arr['target'] = $item['target']; + + $arr['tgt_type'] = 'Collection'; + $arr['target'] = [ + 'id' => str_replace('/item/', '/conversation/', $arr['parent_mid']), + 'type' => 'Collection', + 'attributedTo' => channel_url($channel['channel_address']) + ]; $arr['term'] = $item['term']; @@ -3286,10 +3436,14 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false $post = item_store($arr); } - $post_id = $post['item_id']; + $post_id = $post['item_id'] ?? 0; + $approval_id = $post['approval_id'] ?? 0; if($post_id) { Master::Summon([ 'Notifier','tgroup',$post_id ]); + if ($approval_id) { + Master::Summon(['Notifier', 'tgroup', $approval_id]); + } } return; } @@ -3314,14 +3468,14 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false if ($edit) { if (intval($item['item_deleted'])) { - drop_item($item['id'],false,DROPITEM_PHASE1); - Master::Summon([ 'Notifier','drop',$item['id'] ]); + drop_item($item['id'], DROPITEM_PHASE1, uid: $item['uid']); + Master::Summon(['Notifier', 'drop', $item['id']]); return; } return; } else { - $arr['uuid'] = item_message_id(); + $arr['uuid'] = uuid_from_url($item['mid']); $arr['mid'] = z_root() . '/activity/' . $arr['uuid']; $arr['parent_mid'] = $item['parent_mid']; //IConfig::Set($arr,'activitypub','context', str_replace('/item/','/conversation/',$item['parent_mid'])); @@ -3365,12 +3519,12 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false $arr['deny_gid'] = $channel['channel_deny_gid']; $arr['comment_policy'] = map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments')); - $post = item_store($arr); - $post_id = $post['item_id']; + $post = item_store($arr, deliver: false, addAndSync: false); + $post_id = $post['item_id'] ?? 0; - if ($post_id) { - Master::Summon([ 'Notifier','tgroup',$post_id ]); - } + if ($post_id) { + Master::Summon(['Notifier', 'tgroup', $post_id]); + } q("update channel set channel_lastpost = '%s' where channel_id = %d", dbesc(datetime_convert()), @@ -3380,81 +3534,6 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false return; } - - // Change this copy of the post to a forum head message and deliver to all the tgroup members - // also reset all the privacy bits to the forum default permissions - - $private = (($channel['channel_allow_cid'] || $channel['channel_allow_gid'] - || $channel['channel_deny_cid'] || $channel['channel_deny_gid']) ? 1 : 0); - - $new_public_policy = map_scope(PermissionLimits::Get($channel['channel_id'],'view_stream'),true); - - if((! $private) && $new_public_policy) - $private = 1; - - $item_wall = 1; - $item_origin = (($item['item_deleted']) ? 0 : 1); // item_origin for deleted items is set to 0 in delete_imported_item() to prevent looping. In this case we probably should not set it back to 1 here. - $item_uplink = 0; - $item_nocomment = 0; - - $flag_bits = $item['item_flags']; - - // maintain the original source, which will be the original item owner and was stored in source_xchan - // when we created the delivery fork - - if($parent) { - $r = q("update item set source_xchan = '%s' where id = %d", - dbesc($parent['source_xchan']), - intval($item_id) - ); - } - else { - $item_uplink = (($item['item_rss']) ? 0 : 1); // Do not set item_uplink for rss items - we can not send anything to them. - - // if this is an edit, item_store_update() will have already updated the item - // with the correct value for source_xchan (by ignoring it). We cannot set to owner_xchan - // in this case because owner_xchan will point to the parent of this chain - // and not the original sender. - - if(! $edit) { - $r = q("update item set source_xchan = owner_xchan where id = %d", - intval($item_id) - ); - } - } - - $title = $item['title']; - $body = $item['body']; - - $r = q("update item set item_uplink = %d, item_nocomment = %d, item_flags = %d, owner_xchan = '%s', allow_cid = '%s', allow_gid = '%s', - deny_cid = '%s', deny_gid = '%s', item_private = %d, public_policy = '%s', comment_policy = '%s', title = '%s', body = '%s', item_wall = %d, item_origin = %d where id = %d", - intval($item_uplink), - intval($item_nocomment), - intval($flag_bits), - dbesc($channel['channel_hash']), - dbesc($channel['channel_allow_cid']), - dbesc($channel['channel_allow_gid']), - dbesc($channel['channel_deny_cid']), - dbesc($channel['channel_deny_gid']), - intval($private), - dbesc($new_public_policy), - dbesc(map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments'))), - dbesc($title), - dbesc($body), - intval($item_wall), - intval($item_origin), - intval($item_id) - ); - - if($r) - Master::Summon([ 'Notifier','tgroup',$item_id ]); - else { - logger('start_delivery_chain: failed to update item'); - // reset the source xchan to prevent loops - $r = q("update item set source_xchan = '' where id = %d", - intval($item_id) - ); - } } /** @@ -3793,7 +3872,7 @@ function item_expire($uid,$days,$comment_days = 7) { if ($r) { foreach ($r as $item) { - drop_item($item['id'], false); + drop_item($item['id'], uid: $uid); } } @@ -3806,25 +3885,12 @@ function retain_item($id) { ); } -function drop_items($items,$interactive = false,$stage = DROPITEM_NORMAL) { - $uid = 0; - - if(! local_channel() && ! remote_channel()) - return; - - if(count($items)) { +function drop_items($items, $stage = DROPITEM_NORMAL, $force = false, $expire = false) { + if ($items) { foreach($items as $item) { - $owner = drop_item($item,$interactive,$stage); - if($owner && ! $uid) - $uid = $owner; + drop_item($item, $stage, $force, expire: $expire); } } - - // multiple threads may have been deleted, send an expire notification - - if($uid) { - Master::Summon([ 'Notifier','expire',$uid ]); - } } @@ -3837,7 +3903,7 @@ function drop_items($items,$interactive = false,$stage = DROPITEM_NORMAL) { // $stage = 1 => set deleted flag on the item and perform intial notifications // $stage = 2 => perform low level delete at a later stage -function drop_item($id,$interactive = true,$stage = DROPITEM_NORMAL) { +function drop_item($id, $stage = DROPITEM_NORMAL, $force = false, $uid = 0, $observer_hash = '', $expire = false, $recurse = false) { // locate item to be deleted @@ -3845,33 +3911,48 @@ function drop_item($id,$interactive = true,$stage = DROPITEM_NORMAL) { intval($id) ); - if((! $r) || (intval($r[0]['item_deleted']) && ($stage === DROPITEM_NORMAL))) { - if(! $interactive) - return 0; - notice( t('Item not found.') . EOL); - //goaway(z_root() . '/' . $_SESSION['return_url']); + if(!$r || (intval($r[0]['item_deleted']) && $stage === DROPITEM_NORMAL)) { + return false; } $item = $r[0]; - $ok_to_delete = false; + if(!$recurse) { + drop_related($item, $stage, $force, $uid, $observer_hash, $expire); + } - // system deletion - if(! $interactive) - $ok_to_delete = true; + $ok_to_delete = false; // admin deletion - if(is_site_admin()) + if(is_site_admin()) { $ok_to_delete = true; + } // owner deletion - if(local_channel() && local_channel() == $item['uid']) + if(local_channel() && local_channel() == $item['uid']) { $ok_to_delete = true; + } + + // remote delete when nobody is authenticated (called from Libzot and Daemons) + if ($uid && intval($uid) === intval($item['uid'])) { + $ok_to_delete = true; + } // author deletion - $observer = App::get_observer(); - if($observer && $observer['xchan_hash'] && ($observer['xchan_hash'] === $item['author_xchan'])) + if ($observer_hash) { + $observer = ['xchan_hash' => $observer_hash]; + } + else { + $observer = App::get_observer(); + } + + if (isset($observer['xchan_hash']) && $observer['xchan_hash'] === $item['author_xchan']) { $ok_to_delete = true; + } + + if (isset($observer['xchan_hash']) && $observer['xchan_hash'] === $item['owner_xchan']) { + $ok_to_delete = true; + } if($ok_to_delete) { @@ -3884,9 +3965,9 @@ function drop_item($id,$interactive = true,$stage = DROPITEM_NORMAL) { $arr = [ 'item' => $item, - 'interactive' => $interactive, 'stage' => $stage ]; + /** * @hooks drop_item * Called when an 'item' is removed. @@ -3909,30 +3990,95 @@ function drop_item($id,$interactive = true,$stage = DROPITEM_NORMAL) { delete_item_lowlevel($item, $stage); } - if(! $interactive) - return 1; + return true; + } + else { + return false; + } +} - // send the notification upstream/downstream as the case may be - // only send notifications to others if this is the owner's wall item. - // This isn't optimal. We somehow need to pass to this function whether or not - // to call the notifier, or we need to call the notifier from the calling function. - // We'll rely on the undocumented behaviour that DROPITEM_PHASE1 is (hopefully) only - // set if we know we're going to send delete notifications out to others. +// If somebody deletes a 'Create' activity, find any associated 'Add/Collection' +// activity and delete it. And vice versa. - if((intval($item['item_wall']) && ($stage != DROPITEM_PHASE2)) || ($stage == DROPITEM_PHASE1)) { - Master::Summon([ 'Notifier','drop',$notify_id ]); +function drop_related($item, $stage = DROPITEM_NORMAL, $force = false, $uid = 0, $observer_hash = '', $expire = false, $recurse = false) { + $allRelated = q("select * from item where parent_mid = '%s' and uid = %d", + dbesc($item['parent_mid']), + intval($item['uid']) + ); + if (! $allRelated) { + return; + } + if ($item['verb'] === 'Add' && $item['tgt_type'] === 'Collection') { + if (is_array($item['obj'])) { + $thisItem = $item['obj']; + } + else { + $thisItem = json_decode($item['obj'], true); + } + if (isset($thisItem['object']['id'])) { + $targetMid = $thisItem['object']['id']; + } + if (!$targetMid) { + return; + } + foreach ($allRelated as $related) { + if ($related['mid'] === $targetMid) { + drop_item($related['id'], $stage, $force, $uid, $observer_hash, $expire, recurse: true); + break; + } } - //goaway(z_root() . '/' . $_SESSION['return_url']); } else { - if(! $interactive) - return 0; - notice( t('Permission denied.') . EOL); - //goaway(z_root() . '/' . $_SESSION['return_url']); + foreach ($allRelated as $related) { + if ($related['verb'] === 'Add' && str_contains($related['tgt_type'], 'Collection')) { + $thisItem = json_decode($related['obj'], true); + if (isset($thisItem['id']) && $thisItem['id'] === str_replace('/item/', '/activity/', $item['mid'])) { + drop_item($related['id'], $stage, $force, $uid, $observer_hash, $expire, recurse: true); + break; + } + } + } + } +} + + +function find_related($item) { + $allRelated = q("select * from item where parent_mid = '%s' and uid = %d", + dbesc($item['parent_mid']), + intval($item['uid']) + ); + if (! $allRelated) { + return false; } + if ($item['verb'] === 'Add' && $item['tgt_type'] === 'Collection') { + $thisItem = json_decode($item['obj'],true); + if (is_array($thisItem)) { + $targetMid = $thisItem['object']['id']; + } + if (!$targetMid) { + return false; + } + foreach ($allRelated as $related) { + if ($related['mid'] === $targetMid) { + return $related; + } + } + } + else { + foreach ($allRelated as $related) { + if ($related['verb'] === 'Add' && str_contains($related['tgt_type'], 'Collection')) { + $thisItem = json_decode($related['obj'], true); + if (isset($thisItem['object']['id']) && $thisItem['object']['id'] === $item['mid']) { + return $related; + } + } + } + } + return false; } + /** * @warning This function does not check for permission and does not send * notifications and does not check recursion. @@ -4656,19 +4802,19 @@ function items_fetch($arr,$channel = null,$observer_hash = null,$client_mode = C return $items; } -function webpage_to_namespace($webpage) { +function item_type_to_namespace($item_type) { - if($webpage == ITEM_TYPE_WEBPAGE) + if($item_type == ITEM_TYPE_WEBPAGE) $page_type = 'WEBPAGE'; - elseif($webpage == ITEM_TYPE_BLOCK) + elseif($item_type == ITEM_TYPE_BLOCK) $page_type = 'BUILDBLOCK'; - elseif($webpage == ITEM_TYPE_PDL) + elseif($item_type == ITEM_TYPE_PDL) $page_type = 'PDL'; - elseif($webpage == ITEM_TYPE_CARD) + elseif($item_type == ITEM_TYPE_CARD) $page_type = 'CARD'; - elseif($webpage == ITEM_TYPE_ARTICLE) + elseif($item_type == ITEM_TYPE_ARTICLE) $page_type = 'ARTICLE'; - elseif($webpage == ITEM_TYPE_DOC) + elseif($item_type == ITEM_TYPE_DOC) $page_type = 'docfile'; else $page_type = 'unknown'; @@ -4677,12 +4823,12 @@ function webpage_to_namespace($webpage) { } -function update_remote_id($channel,$post_id,$webpage,$pagetitle,$namespace,$remote_id,$mid) { +function update_remote_id($channel,$post_id,$item_type,$pagetitle,$namespace,$remote_id,$mid) { if(! intval($post_id)) return; - $page_type = webpage_to_namespace($webpage); + $page_type = item_type_to_namespace($item_type); if($page_type == 'unknown' && $namespace && $remote_id) { $page_type = $namespace; @@ -5008,7 +5154,7 @@ function fix_attached_permissions($uid, $body, $str_contact_allow, $str_group_al $attach = array_shift($attach_q); - $new_public = !(($str_contact_allow || $str_group_allow || $str_contact_deny || $str_group_deny)); + //$new_public = !(($str_contact_allow || $str_group_allow || $str_contact_deny || $str_group_deny)); $existing_public = !(($attach['allow_cid'] || $attach['allow_gid'] || $attach['deny_cid'] || $attach['deny_gid'])); if ($existing_public) { @@ -5023,10 +5169,11 @@ function fix_attached_permissions($uid, $body, $str_contact_allow, $str_group_al continue; } - $item_private = 0; - - if ($new_public === false) { - $item_private = (($str_group_allow || ($str_contact_allow && substr_count($str_contact_allow,'<') > 2)) ? 1 : 2); + if ($token) { + $str_contact_allow = $attach['allow_cid']; + $str_group_allow = $attach['allow_gid']; + $str_contact_deny = $attach['deny_cid']; + $str_group_deny = $attach['deny_gid']; // preserve any existing tokens that may have been set for this file $token_matches = null; @@ -5054,7 +5201,7 @@ function fix_attached_permissions($uid, $body, $str_contact_allow, $str_group_al ); if ($attach['is_photo']) { - $r = q("UPDATE photo SET allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s' + q("UPDATE photo SET allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s' WHERE resource_id = '%s' AND uid = %d ", dbesc($str_contact_allow), dbesc($str_group_allow), @@ -5064,7 +5211,16 @@ function fix_attached_permissions($uid, $body, $str_contact_allow, $str_group_al intval($uid) ); - $r = q("UPDATE item SET allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s', item_private = %d + $item_private = 0; + + if ($str_group_allow || $str_contact_deny || $str_group_deny) { + $item_private = 1; + } + elseif ($str_contact_allow) { + $item_private = 2; + } + + q("UPDATE item SET allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s', item_private = %d WHERE resource_id = '%s' AND 'resource_type' = 'photo' AND uid = %d", dbesc($str_contact_allow), dbesc($str_group_allow), @@ -5085,7 +5241,7 @@ function fix_attached_permissions($uid, $body, $str_contact_allow, $str_group_al * which will allow you to interact with it. */ -function copy_of_pubitem($channel,$mid) { +function copy_of_pubitem($channel, $mid) { $result = null; $syschan = get_sys_channel(); @@ -5103,36 +5259,517 @@ function copy_of_pubitem($channel,$mid) { return $item[0]; } - - $r = q("select * from item where parent_mid = (select parent_mid from item where mid = '%s' and uid = %d ) order by id ", + $r = q("select * from item where parent_mid = (select parent_mid from item where mid = '%s' and uid = %d) and uid = %d order by id ", dbesc($mid), + intval($syschan['channel_id']), intval($syschan['channel_id']) ); if($r) { $items = fetch_post_tags($r,true); foreach($items as $rv) { - $d = q("select id from item where mid = '%s' and uid = %d limit 1", - dbesc($rv['mid']), - intval($channel['channel_id']) - ); - if($d) { - continue; - } unset($rv['id']); unset($rv['parent']); + $rv['aid'] = $channel['channel_account_id']; $rv['uid'] = $channel['channel_id']; $rv['item_wall'] = 0; $rv['item_origin'] = 0; - $x = item_store($rv); + $x = item_store($rv, deliver: false, addAndSync: false); if($x['item_id'] && $x['item']['mid'] === $mid) { $result = $x['item']; + sync_an_item($channel['channel_id'], $x['item_id']); } } } + return $result; } + +function addToCollectionAndSync($ret) { + if (!$ret['success']) { + return $ret; + } + + $channel = channelx_by_n($ret['item']['uid']); + if ($channel && $channel['channel_hash'] === $ret['item']['owner_xchan']) { + $items = [$ret['item']]; + + if ((int)$items[0]['item_blocked'] === ITEM_MODERATED + || (int)$items[0]['item_unpublished'] || (int)$items[0]['item_delayed']) { + return $ret; + } + + xchan_query($items); + // TODO: fetch_post_tags() will add term and iconfig twice if called twice and it looks like they are already added here + // $items = fetch_post_tags($items); + $sync_items = []; + $sync_items[] = encode_item($items[0], true); + + if (!in_array($ret['item']['verb'], ['Add', 'Remove'])) { + $activity = Activity::encode_activity($items[0]); + + if (!$activity) { + return $ret; + } + + $new_obj = Activity::build_packet($activity, $channel, false); + $approval = Activity::addToCollection($channel, $new_obj, $ret['item']['parent_mid'], $ret['item'], deliver: false); + + if ($approval['success']) { + $ret['approval_id'] = $approval['item_id']; + $ret['approval'] = $approval['activity']; + $add_items = [$approval['activity']]; + xchan_query($add_items); + $add_items = fetch_post_tags($add_items); + $sync_items[] = encode_item($add_items[0], true); + } + } + + $resource_type = $ret['item']['resource_type']; + + if ($resource_type === 'event') { + $z = q("select * from event where event_hash = '%s' and uid = %d limit 1", + dbesc($ret['item']['resource_id']), + intval($channel['channel_id']) + ); + + if ($z) { + Libsync::build_sync_packet($channel['channel_id'], ['event_item' => $sync_items, 'event' => $z]); + } + } + elseif ($resource_type === 'photo') { + // reserved for future use, currently handled in the photo upload workflow + } + else { + Libsync::build_sync_packet($ret['item']['uid'], ['item' => $sync_items]); + } + } + + return $ret; +} + +function reverse_activity_mid($string) { + return str_replace(z_root() . '/activity/', z_root() . '/item/', $string); +} + +function set_activity_mid($string) { + return str_replace(z_root() . '/item/', z_root() . '/activity/', $string); +} + +/** + * @brief returns an item by id and parent belonging to local_channel() + * including activity counts. + * @param int $id + * @param int $parent + */ + +function item_by_item_id(int $id, int $parent): array +{ + if (!$id && !$parent && !local_channel()) { + return []; + } + + $item_normal_sql = item_normal(); + + $reaction = item_reaction_sql($parent); + $reaction_cte_sql = $reaction['cte']; + $reaction_select_sql = $reaction['select']; + $reaction_join_sql = $reaction['join']; + + return q("WITH + $reaction_cte_sql + SELECT + *, + $reaction_select_sql + FROM item + $reaction_join_sql + WHERE + item.id = %d + AND item.uid = %d + AND item.verb IN ('Create', 'Update', 'EmojiReact') + $item_normal_sql", + intval($id), + intval(local_channel()) + ); +} + + +/** + * @brief returns an array of items by ids + * ATTENTION: no permissions for the parents are checked here!!! + * Permissions MUST be checked by the module which calls this function. + * @param array $parents + * @param null|array $thr_parents (optional) - thr_parent mids which will be included + * @param string $permission_sql (optional) - SQL as provided by item_permission_sql() from the calling module + * @param bool $blog_mode (optional) - if set to yes only the parent items will be returned + */ + +function items_by_parent_ids(array $parents, null|array $thr_parents = null, string $permission_sql = '', bool $blog_mode = false): array +{ + if (!$parents) { + return []; + } + + $ids = ids_to_querystr($parents, 'item_id'); + $thread_allow = ((local_channel()) ? PConfig::Get(local_channel(), 'system', 'thread_allow', true) : Config::Get('system', 'thread_allow', true)); + $item_normal_sql = item_normal(); + $limit = $thread_allow ? 3 : 1000; + + $thr_parent_sql = (($thread_allow) ? " AND item.thr_parent = item.parent_mid " : ''); + if ($thr_parents && $thread_allow) { + $limit = 300; + $thr_parent_str = stringify_array($thr_parents, true); + $thr_parent_sql = " AND item.thr_parent IN (" . protect_sprintf($thr_parent_str) . ") "; + } + + $reaction = item_reaction_sql($ids, $permission_sql, 'final_selection'); + $reaction_cte_sql = $reaction['cte']; + $reaction_select_sql = $reaction['select']; + $reaction_join_sql = $reaction['join']; + + if ($blog_mode) { + $q = <<<SQL + WITH + final_selection AS ( + SELECT + item.* + FROM + item + WHERE + item.id IN ($ids) + ), + + $reaction_cte_sql + + SELECT + final_selection.*, + $reaction_select_sql + FROM final_selection + $reaction_join_sql + SQL; + + return dbq(trim($q)); + } + + $q = <<<SQL + WITH + parent_items AS ( + SELECT + item.*, + 0 AS rn + FROM item + WHERE + item.id IN ($ids) + ), + + $reaction_cte_sql, + + all_comments AS ( + SELECT + item.*, + ROW_NUMBER() OVER (PARTITION BY item.parent ORDER BY item.created DESC) AS rn + FROM item + WHERE item.parent IN ($ids) + AND item.verb IN ('Create', 'Update', 'EmojiReact') + AND item.item_thread_top = 0 + $thr_parent_sql + $permission_sql + $item_normal_sql + ), + + final_selection AS ( + SELECT * FROM parent_items + UNION ALL + SELECT * FROM all_comments WHERE all_comments.rn <= $limit + ) + + SELECT + final_selection.*, + $reaction_select_sql + FROM final_selection + $reaction_join_sql + SQL; + + return dbq(trim($q)); +} + +/** + * @brief prepare reaction sql for items_by_parent_ids() + * ATTENTION: no permissions for the pa are checked here!!! + * Permissions MUST be checked by the function which returns the ids. + * @param string $ids + * @param string $permission_sql (optional) - SQL provided by item_permission_sql() + * @param string $join_prefix (optional) - prefix for the join part defaults to 'item' + */ + +function item_reaction_sql(string $ids, string $permission_sql = '', string $join_prefix = 'item'): array +{ + $item_normal_sql = item_normal(); + $observer = get_observer_hash(); + + $verbs = [ + 'like' => ['Like'], + 'dislike' => ['Dislike'], + 'announce' => ['Announce'], + 'accept' => ['Accept'], + 'reject' => ['Reject'], + 'tentativeaccept' => ['TentativeAccept'] + ]; + + $thread_allow = ((local_channel()) ? PConfig::Get(local_channel(), 'system', 'thread_allow', true) : Config::Get('system', 'thread_allow', true)); + + if ($thread_allow) { + $verbs['comment'] = ['Create', 'Update', 'EmojiReact']; + } + + $cte = ''; + $select = ''; + $join = ''; + + foreach($verbs as $k => $v) { + + $observer_sql = "0 AS observer_{$k}_count"; + if ($observer) { + $observer_sql = "COUNT(CASE WHEN item.author_xchan = '$observer' THEN 1 END) AS observer_{$k}_count"; + } + + $verbs_str = stringify_array($v); + + if ($cte) { + $cte .= ",\n"; + } + + $cte .= <<<SQL + reaction_{$k} AS ( + SELECT + item.thr_parent, + -- COUNT(DISTINCT item.author_xchan) AS {$k}_count, (should we prevent multiple reactions by the same author?) + COUNT(*) AS {$k}_count, + $observer_sql + FROM item + WHERE item.verb IN ($verbs_str) + AND item.item_thread_top = 0 + AND item.parent IN ($ids) + $item_normal_sql + $permission_sql + GROUP BY item.thr_parent + ) + SQL; + + if ($select) { + $select .= ",\n"; + } + + $select .= <<<SQL + COALESCE(reaction_{$k}.{$k}_count, 0) AS {$k}_count, + COALESCE(reaction_{$k}.observer_{$k}_count, 0) AS observer_{$k}_count + SQL; + + $join .= <<<SQL + LEFT JOIN reaction_{$k} ON reaction_{$k}.thr_parent = $join_prefix.mid + SQL; + + } + + $ret['cte'] = $cte; + $ret['select'] = $select; + $ret['join'] = $join; + + return $ret; +} + + + +/** + * @brief returns an array of items by thr_parent mid of a parent + + * @param string $mid + * @param int $parent + * @param int|null $offset + */ + +function items_by_thr_parent(string $mid, int $parent, int|null $offset = null): array +{ + if (!$mid && !$parent) { + return []; + } + + $parent_item = q("SELECT uid FROM item WHERE id = %d", + intval($parent) + ); + + $order_sql = "ORDER BY item.created"; + if (isset($offset)) { + $order_sql = "ORDER BY item.created DESC, item.received DESC LIMIT 3 OFFSET $offset"; + } + + $owner_uid = intval($parent_item[0]['uid']); + $item_normal_sql = item_normal($owner_uid); + + if (local_channel() === $owner_uid) { + $reaction = item_reaction_sql($parent); + $reaction_cte_sql = $reaction['cte']; + $reaction_select_sql = $reaction['select']; + $reaction_join_sql = $reaction['join']; + + $ret = q("WITH + $reaction_cte_sql + SELECT + item.*, + $reaction_select_sql + FROM item + $reaction_join_sql + WHERE + item.thr_parent = '%s' + AND item.uid = %d + AND item.verb IN ('Create', 'Update', 'EmojiReact') + AND item.item_thread_top = 0 + $item_normal_sql + $order_sql", + dbesc($mid), + intval($owner_uid) + ); + } + else { + $observer_hash = get_observer_hash(); + $permission_sql = item_permissions_sql($owner_uid, $observer_hash); + + $reaction = item_reaction_sql($parent, $permission_sql); + $reaction_cte_sql = $reaction['cte']; + $reaction_select_sql = $reaction['select']; + $reaction_join_sql = $reaction['join']; + + $ret = q("WITH + $reaction_cte_sql + SELECT + item.*, + $reaction_select_sql + FROM item + $reaction_join_sql + WHERE + item.thr_parent = '%s' + AND item.uid = %d + AND item.verb IN ('Create', 'Update', 'EmojiReact') + AND item.item_thread_top = 0 + $permission_sql + $item_normal_sql + $order_sql", + dbesc($mid), + intval($owner_uid) + ); + } + + if (isset($offset)) { + $ret = array_reverse($ret); + } + + return $ret; +} + + +/** + * @brief returns an array of xchan entries (partly) for activities of an item by mid of a parent. + * Also checks if observer is allowed to add activities to the item. + * @param string $mid + * @param int $parent + * @param string $verb + */ + +function item_activity_xchans(string $mid, int $parent, string $verb): array +{ + if (!$mid && !$parent && !$verb) { + return []; + } + + $observer_hash = get_observer_hash(); + $parent_item = q("SELECT * FROM item WHERE id = %d", + intval($parent) + ); + + $owner_uid = intval($parent_item[0]['uid']); + $item_normal = item_normal($owner_uid); + + if (local_channel() === $owner_uid) { + $ret = q("SELECT item.id, item.item_blocked, xchan.xchan_hash, xchan.xchan_name as name, xchan.xchan_url as url, xchan.xchan_photo_s as photo FROM item + LEFT JOIN xchan ON item.author_xchan = xchan.xchan_hash + WHERE item.uid = %d + AND item.parent = %d + AND item.thr_parent = '%s' + AND item.verb = '%s' + AND item.item_thread_top = 0 + $item_normal + -- GROUP BY item.author_xchan (should we prevent multiple reactions by the same author?) + ORDER BY item.created", + intval(local_channel()), + intval($parent), + dbesc($mid), + dbesc($verb) + ); + } + else { + $sql_extra = item_permissions_sql($owner_uid, $observer_hash); + + $ret = q("SELECT item.id, item.item_blocked, xchan.xchan_hash, xchan.xchan_name as name, xchan.xchan_url as url, xchan.xchan_photo_s as photo FROM item + LEFT JOIN xchan ON item.author_xchan = xchan.xchan_hash + WHERE item.uid = %d + AND item.thr_parent = '%s' + AND item.verb = '%s' + AND item.item_thread_top = 0 + $sql_extra + $item_normal + -- GROUP BY item.author_xchan (should we prevent multiple reactions by the same author?) + ORDER BY item.created", + intval($owner_uid), + dbesc($mid), + dbesc($verb) + ); + } + + $ret['is_commentable'] = can_comment_on_post($observer_hash, $parent_item[0]); + + return $ret; +} + + +/** + * @brief find and return thr_parents we need to show when displaying a nested comment. + * TODO: can this be improved or maybe implemented differently in the UI? + * @param array $item + */ + +function get_recursive_thr_parents(array $item): array|null +{ + if ($item['id'] === $item['parent']) { + // This is a toplevel post, return null. + return null; + } + + $thr_parents[] = $item['thr_parent']; + + $mid = $item['thr_parent']; + $parent_mid = $item['parent_mid']; + $uid = $item['uid']; + $i = 0; + + while ($mid !== $item['parent_mid'] && $i < 100) { + $x = q("SELECT thr_parent, mid FROM item WHERE uid = %d AND mid = '%s'", + intval($uid), + dbesc($mid) + ); + + if (!$x) { + break; + } + + $mid = $x[0]['thr_parent']; + $thr_parents[] = $x[0]['thr_parent']; + + $i++; + } + + return $thr_parents; +} |