FEP-5624 continued

This commit is contained in:
Mike Macgirvin 2022-12-12 06:16:17 +11:00
parent 69a2652c91
commit ee19699827
6 changed files with 361 additions and 40 deletions

View file

@ -2,6 +2,7 @@
namespace Code\Daemon; namespace Code\Daemon;
use Code\Lib\IConfig;
use Code\Lib\Libzot; use Code\Lib\Libzot;
use Code\Lib\Queue; use Code\Lib\Queue;
use Code\Lib\Activity; use Code\Lib\Activity;
@ -387,17 +388,25 @@ class Notifier implements DaemonInterface
if (($relay_to_owner || $uplink) && ($cmd !== 'relay')) { if (($relay_to_owner || $uplink) && ($cmd !== 'relay')) {
logger('followup relay (upstream delivery)', LOGGER_DEBUG); logger('followup relay (upstream delivery)', LOGGER_DEBUG);
$comment_recipient = IConfig::Get($target_item['id'], 'system', 'comment_recipient');
if ($comment_recipient) {
$sendto = $comment_recipient;
self::$recipients = [$comment_recipient];
self::$private = false;
}
else {
$sendto = ($uplink) ? $parent_item['source_xchan'] : $parent_item['owner_xchan']; $sendto = ($uplink) ? $parent_item['source_xchan'] : $parent_item['owner_xchan'];
self::$recipients = [ $sendto ]; self::$recipients = [$sendto];
// over-ride upstream recipients if 'replyTo' was set in the parent. // over-ride upstream recipients if 'replyTo' was set in the parent.
if ($parent_item['replyto'] && (! $uplink)) { if ($parent_item['replyto'] && (!$uplink)) {
logger('replyto: over-riding owner ' . $sendto, LOGGER_DEBUG); logger('replyto: over-riding owner ' . $sendto, LOGGER_DEBUG);
// unserialise is a no-op if presented with data that wasn't serialised. // unserialise is a no-op if presented with data that wasn't serialised.
$ptr = unserialise($parent_item['replyto']); $ptr = unserialise($parent_item['replyto']);
if (is_string($ptr)) { if (is_string($ptr)) {
if (ActivityStreams::is_url($sendto)) { if (ActivityStreams::is_url($sendto)) {
$sendto = $ptr; $sendto = $ptr;
self::$recipients = [ $sendto ]; self::$recipients = [$sendto];
} }
} elseif (is_array($ptr)) { } elseif (is_array($ptr)) {
$sendto = []; $sendto = [];
@ -411,20 +420,20 @@ class Notifier implements DaemonInterface
self::$recipients = $sendto; self::$recipients = $sendto;
} }
} }
}
logger('replyto: upstream recipients ' . print_r($sendto, true), LOGGER_DEBUG); logger('replyto: upstream recipients ' . print_r($sendto, true), LOGGER_DEBUG);
self::$private = true; self::$private = true;
$upstream = true; $upstream = true;
self::$packet_type = 'response'; self::$packet_type = 'response';
$is_moderated = their_perms_contains($parent_item['uid'], $sendto, 'moderated'); $is_moderated = their_perms_contains($parent_item['uid'], (is_array($sendto) ? $sendto[0] : $sendto), 'moderated');
if ($relay_to_owner && $thread_is_public && (! $is_moderated) && (! $question) && (! Channel::is_group($parent_item['uid']))) { if ($relay_to_owner && $thread_is_public && $target_item['approved'] && (! $is_moderated) && (! $question) && (! Channel::is_group($parent_item['uid']))) {
if (get_pconfig($target_item['uid'], 'system', 'hyperdrive', true)) { if (get_pconfig($target_item['uid'], 'system', 'hyperdrive', true)) {
Run::Summon([ 'Notifier' , 'hyper', $item_id ]); Run::Summon([ 'Notifier' , 'hyper', $item_id ]);
} }
} }
} else { }
else {
if ($cmd === 'relay') { if ($cmd === 'relay') {
logger('owner relay (downstream delivery)'); logger('owner relay (downstream delivery)');
} else { } else {
@ -451,7 +460,8 @@ class Notifier implements DaemonInterface
} }
} }
if ($thread_is_public && $cmd === 'hyper') {
if ($thread_is_public && $target_item['approved'] && $cmd === 'hyper') {
self::$recipients = []; self::$recipients = [];
$r = q( $r = q(
"select abook_xchan, xchan_network from abook left join xchan on abook_xchan = xchan_hash where abook_channel = %d and abook_self = 0 and abook_pending = 0 and abook_archived = 0 and not abook_xchan in ( '%s', '%s', '%s' ) ", "select abook_xchan, xchan_network from abook left join xchan on abook_xchan = xchan_hash where abook_channel = %d and abook_self = 0 and abook_pending = 0 and abook_archived = 0 and not abook_xchan in ( '%s', '%s', '%s' ) ",
@ -465,8 +475,8 @@ class Notifier implements DaemonInterface
self::$recipients[] = $rv['abook_xchan']; self::$recipients[] = $rv['abook_xchan'];
} }
} }
self::$private = false; }
} else { else {
self::$private = false; self::$private = false;
self::$recipients = collect_recipients($parent_item, self::$private); self::$recipients = collect_recipients($parent_item, self::$private);
} }

View file

@ -73,7 +73,7 @@ class Activity
} }
public static function fetch($url, $channel = null, $debug = false) public static function fetch($url, $channel = null, $must_verify = false, $debug = false)
{ {
if (!$url) { if (!$url) {
return null; return null;
@ -154,7 +154,9 @@ class Activity
if (($y['type']) && (!ActivityStreams::is_an_actor($y['type']))) { if (($y['type']) && (!ActivityStreams::is_an_actor($y['type']))) {
$sigblock = HTTPSig::verify($x); $sigblock = HTTPSig::verify($x);
if ($must_verify && !$sigblock['header_signed']) {
return null;
}
if (($sigblock['header_signed']) && (!$sigblock['header_valid'])) { if (($sigblock['header_signed']) && (!$sigblock['header_valid'])) {
if ($debug) { if ($debug) {
return array_merge($x, $sigblock); return array_merge($x, $sigblock);
@ -808,6 +810,11 @@ class Activity
$activity['inReplyTo'] = $item['thr_parent']; $activity['inReplyTo'] = $item['thr_parent'];
} }
// For comment approvals and rejections
if (in_array($activity['type'], ['Accept','Reject']) && is_string($item['obj']) && strlen($item['obj'])) {
$activity['inReplyTo'] = $item['thr_parent'];
}
$cnv = get_iconfig($item['parent'], 'activitypub', 'context'); $cnv = get_iconfig($item['parent'], 'activitypub', 'context');
if (!$cnv) { if (!$cnv) {
$cnv = $activity['parent_mid']; $cnv = $activity['parent_mid'];
@ -3002,6 +3009,7 @@ class Activity
} }
} }
if (!(array_key_exists('created', $s) && $s['created'])) { if (!(array_key_exists('created', $s) && $s['created'])) {
$s['created'] = datetime_convert(); $s['created'] = datetime_convert();
} }
@ -3573,6 +3581,7 @@ class Activity
$is_system = Channel::is_system($channel['channel_id']); $is_system = Channel::is_system($channel['channel_id']);
$is_child_node = false; $is_child_node = false;
$commentApproval = null;
// Pleroma scrobbles can be really noisy and contain lots of duplicate activities. Disable them by default. // Pleroma scrobbles can be really noisy and contain lots of duplicate activities. Disable them by default.
@ -3629,7 +3638,46 @@ class Activity
$item['obj_type'] = 'Answer'; $item['obj_type'] = 'Answer';
} }
} }
if ($item['approved']) {
$valid = CommentApproval::verify($item, $channel);
if (!$valid) {
logger('commentApproval failed');
return;
}
}
if (in_array($item['verb'], ['Accept', 'Reject'])) {
$i = q("select * from item where mid = '%s' and uid = %d",
dbesc(is_array($item['obj']) ? $item['obj']['id'] : $item['obj']),
intval($channel['channel_id'])
);
if ($i) {
if ($item['verb'] === 'Accept') {
$valid = CommentApproval::verify($i[0],$channel,$act);
if ($valid) {
q("update item set approved = '%s' where mid = '%s' and uid = %d",
dbesc($item['mid']),
dbesc(is_array($item['obj']) ? $item['obj']['id'] : $item['obj']),
intval($channel['channel_id'])
);
}
}
else {
$valid = CommentApproval::verifyReject($i[0],$channel,$act);
if ($valid) {
q("update item set approved = '%s' where mid = '%s' and uid = %d",
dbesc(''),
dbesc(is_array($item['obj']) ? $item['obj']['id'] : $item['obj']),
intval($channel['channel_id'])
);
}
}
}
}
if (!$item['approved'] && $parent_item['owner_xchan'] === $channel['channel_hash'] && $item['author_xchan'] !== $channel['channel_hash']) {
$commentApproval = new CommentApproval($channel, $item);
}
// quietly reject group comment boosts by group owner // quietly reject group comment boosts by group owner
// (usually only sent via ActivityPub so groups will work on microblog platforms) // (usually only sent via ActivityPub so groups will work on microblog platforms)
// This catches those activities if they slipped in via a conversation fetch // This catches those activities if they slipped in via a conversation fetch
@ -3649,8 +3697,8 @@ class Activity
|| $allowed === 'moderated') { || $allowed === 'moderated') {
$item['item_blocked'] = ITEM_MODERATED; $item['item_blocked'] = ITEM_MODERATED;
} }
if ($item['item_blocked'] !== ITEM_MODERATED) { if ($item['item_blocked'] !== ITEM_MODERATED && $commentApproval) {
self::send_accept_activity($channel, $item['author_xchan'], $item, $parent_item); $commentApproval->Accept();
} }
} }
@ -3658,7 +3706,9 @@ class Activity
logger('rejected comment from ' . $item['author_xchan'] . ' for ' . $channel['channel_address']); logger('rejected comment from ' . $item['author_xchan'] . ' for ' . $channel['channel_address']);
logger('rejected: ' . print_r($item, true), LOGGER_DATA); logger('rejected: ' . print_r($item, true), LOGGER_DATA);
// let the sender know we received their comment, but we don't permit spam here. // let the sender know we received their comment, but we don't permit spam here.
self::send_reject_activity($channel, $item['author_xchan'], $item, $parent_item); if ($commentApproval) {
$commentApproval->Reject();
}
return; return;
} }
@ -3893,12 +3943,12 @@ class Activity
} }
$r = q( $r = q(
"select id, created, edited from item where mid = '%s' and uid = %d limit 1", "select id, created, edited, approved from item where mid = '%s' and uid = %d limit 1",
dbesc($item['mid']), dbesc($item['mid']),
intval($item['uid']) intval($item['uid'])
); );
if ($r) { if ($r) {
if ($item['edited'] > $r[0]['edited']) { if ($item['edited'] > $r[0]['edited'] || $item['approved'] !== $r[0]['approved']) {
$item['id'] = $r[0]['id']; $item['id'] = $r[0]['id'];
ObjCache::Set($item['mid'], $act->raw); ObjCache::Set($item['mid'], $act->raw);
$x = item_store_update($item, deliver: false); $x = item_store_update($item, deliver: false);

View file

@ -0,0 +1,209 @@
<?php
namespace Code\Lib;
use Code\Daemon\Run;
class CommentApproval
{
protected $channel;
protected $item;
public function __construct($channel, $item)
{
$this->channel = $channel;
$this->item = $item;
}
public function Accept()
{
$obj = $this->item['obj'];
if (! is_array($obj)) {
$obj = json_decode($obj, true);
}
$parent = $this->get_parent();
$activity = post_activity_item(
[
'verb' => 'Accept',
'obj_type' => $obj['type'],
'obj' => $obj['id'],
'item_wall' => 1,
'allow_cid' => '',
'owner_xchan' => $this->channel['xchan_hash'],
'author_xchan' => $this->channel['xchan_hash'],
'parent_mid' => $parent,
'thr_parent' => $parent,
'uid' => $this->channel['channel_id'],
'title' => 'comment accepted'
],
deliver: false,
channel: $this->channel,
observer: $this->channel
);
if ($activity['item_id']) {
IConfig::Set($activity['item_id'], 'system', 'comment_recipient', $this->item['author_xchan']);
}
Run::Summon(['Notifier', 'comment_approval', $activity['item_id']]);
}
public function Reject()
{
$obj = $this->item['obj'];
if (! is_array($obj)) {
$obj = json_decode($obj, true);
}
$parent = $this->get_parent();
$activity = post_activity_item(
[
'verb' => 'Reject',
'obj_type' => $obj['type'],
'obj' => $obj['id'],
'item_wall' => 1,
'allow_cid' => '',
'owner_xchan' => $this->channel['xchan_hash'],
'author_xchan' => $this->channel['xchan_hash'],
'parent_mid' => $parent,
'thr_parent' => $parent,
'uid' => $this->channel['channel_id'],
'title' => 'comment rejected'
],
deliver: false,
channel: $this->channel,
observer: $this->channel
);
if ($activity['item_id']) {
IConfig::Set($activity['item_id'], 'system', 'comment_recipient', $this->item['author_xchan']);
}
Run::Summon(['Notifier', 'comment_approval', $activity['item_id']]);
}
/**
* To be considered valid, the Accept activity referenced in replyApproval MUST
* satisfy the following properties:
*
* its actor property is the authority
* its authenticity can be asserted
* its object property is the reply under consideration
* its inReplyTo property matches that of the reply under consideration
*
* In addition, if the reply is considered valid, but has no valid replyApproval
* despite the object it is in reply to having a canReply property, the recipient MAY hide
* the reply from certain views.
*/
public static function verify($item, $channel, $approvalActivity = null)
{
if(!$approvalActivity) {
$approvalActivity = Activity::fetch($item['approved'], $channel, true);
}
$parent_item = q("select * from item where mid = '%s'", $item['parent_mid']);
if (! $approvalActivity) {
logger('no approval activity');
return false;
}
if (!$parent_item) {
logger('no parent item');
return false;
}
if ($approvalActivity instanceof ActivityStreams) {
$act = $approvalActivity;
}
else {
$act = new ActivityStreams($approvalActivity);
}
if (! $act->is_valid()) {
logger('invalid parse');
return false;
}
if (!$act->obj) {
logger('no object');
return false;
}
if ($act->type !== 'Accept') {
logger('not an accept');
return false;
}
if (!isset($act->actor)) {
logger('no actor');
return false;
}
$sender = Activity::find_best_identity($act->actor['id']);
if (!$sender || $sender !== $parent_item[0]['owner_xchan']) {
logger('no identity');
return false;
}
$comment = is_string($act->obj) ? $act->obj : $act->obj['id'];
if ($comment !== $item['mid']) {
logger('incorrect mid');
return false;
}
if(!in_array($act->parent_id, [$item['thr_parent'], $item['parent_mid']])) {
logger('wrong provenance');
logger('act: ' . $act->parent_id);
logger('item: ' . print_r($item,true));
return false;
}
logger('comment verified', LOGGER_DEBUG);
return true;
}
public static function verifyReject($item, $channel, $approvalActivity = null)
{
if(!$approvalActivity) {
$approvalActivity = Activity::fetch($item['approved'], $channel, true);
}
$parent_item = q("select * from item where mid = '%s'", $item['parent_mid']);
if (! $approvalActivity) {
return false;
}
if (!$parent_item) {
return false;
}
if ($approvalActivity instanceof ActivityStreams) {
$act = $approvalActivity;
}
else {
$act = new ActivityStreams($approvalActivity);
}
if (! $act->is_valid()) {
return false;
}
if (!$act->obj) {
return false;
}
if ($act->type !== 'Reject') {
return false;
}
if (!isset($act->actor)) {
return false;
}
$sender = Activity::find_best_identity($act->actor['id']);
if (!$sender || $sender !== $parent_item['owner_xchan']) {
return false;
}
$comment = is_string($act->obj) ? $act->obj : $act->obj['id'];
if ($comment !== $item['mid']) {
return false;
}
if(!in_array($act->parent_id, [$item['thr_parent'], $item['parent_mid']])) {
return false;
}
logger('comment verified', LOGGER_DEBUG);
return true;
}
protected function get_parent()
{
$result = q("select parent_mid from item where mid = '%s'",
dbesc($this->item['parent_mid'])
);
return $result ? $result[0]['parent_mid'] : '';
}
}

View file

@ -1727,13 +1727,63 @@ class Libzot
// perform pre-storage check to see if it's "likely" that this is a group or collection post // perform pre-storage check to see if it's "likely" that this is a group or collection post
if (in_array($arr['verb'], ['Accept', 'Reject'])) {
logger('verifying comment accept/reject');
$i = q("select * from item where mid = '%s' and uid = %d",
dbesc(is_array($arr['obj']) ? $arr['obj']['id'] : $arr['obj']),
intval($channel['channel_id'])
);
logger('haveItem');
if ($i) {
if ($arr['verb'] === 'Accept' && !$i[0]['approved']) {
$valid = CommentApproval::verify($i[0],$channel,$act);
if ($valid) {
q("update item set approved = '%s' where mid = '%s' and uid = %d",
dbesc($arr['mid']),
dbesc(is_array($arr['obj']) ? $arr['obj']['id'] : $arr['obj']),
intval($channel['channel_id'])
);
//Run::Summon(['Notifier', 'activity', $i[0]['id']]);
}
}
elseif ($i[0]['approved']) {
$valid = CommentApproval::verifyReject($i[0],$channel,$act);
if ($valid) {
q("update item set approved = '%s' where mid = '%s' and uid = %d",
dbesc(''),
dbesc(is_array($arr['obj']) ? $arr['obj']['id'] : $arr['obj']),
intval($channel['channel_id'])
);
}
// Run::Summon(['Notifier', 'activity', $i[0]['id']]);
}
// Do not store these
continue;
}
}
$tag_delivery = tgroup_check($channel['channel_id'], $arr); $tag_delivery = tgroup_check($channel['channel_id'], $arr);
$perm = 'send_stream'; $perm = 'send_stream';
if (($arr['mid'] !== $arr['parent_mid']) && ($relay)) { if (($arr['mid'] !== $arr['parent_mid']) && ($relay)) {
$perm = 'post_comments'; $perm = 'post_comments';
if ($arr['approved']) {
$valid = CommentApproval::verify($arr, $channel);
if (!$valid) {
logger('commentApproval failed');
continue;
}
}
if (!$arr['approved'] && $arr['author_xchan'] !== $channel['channel_hash']) {
$commentApproval = new CommentApproval($channel, $arr); $commentApproval = new CommentApproval($channel, $arr);
} }
}
// This is our own post, possibly coming from a channel clone // This is our own post, possibly coming from a channel clone
@ -1818,6 +1868,7 @@ class Libzot
} }
} }
if (!$allowed) { if (!$allowed) {
if ($arr['mid'] !== $arr['parent_mid']) { if ($arr['mid'] !== $arr['parent_mid']) {
if ($commentApproval) { if ($commentApproval) {
@ -1979,7 +2030,7 @@ class Libzot
continue; continue;
} // Maybe it has been edited? } // Maybe it has been edited?
elseif ($arr['edited'] > $r[0]['edited']) { elseif ($arr['edited'] > $r[0]['edited'] || $arr['approved'] !== $r[0]['approved']) {
$arr['id'] = $r[0]['id']; $arr['id'] = $r[0]['id'];
$arr['uid'] = $channel['channel_id']; $arr['uid'] = $channel['channel_id'];
if (post_is_importable($channel['channel_id'], $arr, $abook)) { if (post_is_importable($channel['channel_id'], $arr, $abook)) {

View file

@ -34,7 +34,7 @@ class Ap_probe extends Controller
} }
} }
$j = Activity::fetch($resource, $channel, true); $j = Activity::fetch($resource, $channel, false,true);
if ($j) { if ($j) {
$html .= '<pre>' . str_replace('\\n', "\n", htmlspecialchars(json_encode($j, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT))) . '</pre>'; $html .= '<pre>' . str_replace('\\n', "\n", htmlspecialchars(json_encode($j, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT))) . '</pre>';

View file

@ -377,6 +377,7 @@ function absolutely_no_comments($item) {
*/ */
function post_activity_item($arr, $allow_code = false, $deliver = true, $channel = null, $observer = null) { function post_activity_item($arr, $allow_code = false, $deliver = true, $channel = null, $observer = null) {
logger('input: ' . print_r($arr,true), LOGGER_DATA);
$ret = [ 'success' => false ]; $ret = [ 'success' => false ];
$is_comment = false; $is_comment = false;
@ -436,8 +437,8 @@ function post_activity_item($arr, $allow_code = false, $deliver = true, $channel
$arr['comment_policy'] = map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments')); $arr['comment_policy'] = map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments'));
if ((! $arr['plink']) && (intval($arr['item_thread_top']))) { if (empty($arr['plink'])) {
$arr['plink'] = substr(z_root() . '/channel/' . $channel['channel_address'] . '/?f=&mid=' . urlencode($arr['mid']),0,190); $arr['plink'] = $arr['mid'];
} }
// for the benefit of plugins, we will behave as if this is an API call rather than a normal online post // for the benefit of plugins, we will behave as if this is an API call rather than a normal online post