diff options
28 files changed, 460 insertions, 266 deletions
@@ -1,3 +1,7 @@ +Hubzilla 8.8.8 (2024-02-29) + - Streams compatibility fixes + + Hubzilla 8.8.7 (2024-01-19) - Fix regression in Activity::actor_store() diff --git a/Zotlabs/Daemon/Notifier.php b/Zotlabs/Daemon/Notifier.php index 948aba80c..4e7ca3911 100644 --- a/Zotlabs/Daemon/Notifier.php +++ b/Zotlabs/Daemon/Notifier.php @@ -276,7 +276,7 @@ class Notifier { } // follow/unfollow is for internal use only - if (in_array($target_item['verb'], [ACTIVITY_FOLLOW, ACTIVITY_UNFOLLOW])) { + if (in_array($target_item['verb'], ['Follow', 'Ignore', ACTIVITY_FOLLOW, ACTIVITY_UNFOLLOW])) { logger('not fowarding follow/unfollow note activity'); return; } diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index b4ce4b5d0..1e0ce6ae2 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -172,10 +172,6 @@ class Activity { } static function fetch_person($x) { - return self::fetch_profile($x); - } - - static function fetch_profile($x) { $r = q("select * from xchan where xchan_url = '%s' limit 1", dbesc($x['id']) ); @@ -189,7 +185,14 @@ class Activity { return []; return self::encode_person($r[0]); + } + + static function fetch_profile($x) { + if (isset($x['describes'])) { + return $x; + } + return []; } static function fetch_thing($x) { @@ -978,6 +981,7 @@ class Activity { $ret['to'] = [ACTIVITY_PUBLIC_INBOX]; } + $hookinfo = [ 'item' => $i, 'encoded' => $ret @@ -2049,6 +2053,10 @@ class Activity { $s['mid'] = $act->obj['data']['id']; } + if ($act->objprop('type') === 'Profile') { + $s['mid'] = $act->id; + } + if (!$s['mid']) { return false; } @@ -2293,6 +2301,12 @@ class Activity { if (!$response_activity) { + if ($act->objprop('type') === 'Profile') { + $s['parent_mid'] = $s['mid']; + $s['item_thread_top'] = 1; + } + + // we will need a hook here to extract magnet links e.g. peertube // right now just link to the largest mp4 we find that will fit in our // standard content region @@ -2438,7 +2452,6 @@ class Activity { } } - if ($act->objprop('type') === 'Page' && !$s['body']) { $ptr = null; @@ -2479,7 +2492,7 @@ class Activity { } } - if (in_array($act->objprop('type', ''), ['Note', 'Article', 'Page'])) { + if (in_array($act->objprop('type'), ['Note', 'Article', 'Page'])) { $ptr = null; if (array_key_exists('url', $act->obj)) { diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php index a98ae8f20..942c3082a 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -1143,7 +1143,6 @@ class Libzot { if ($env['encoding'] === 'activitystreams') { $AS = new ActivityStreams($data); - if (!$AS->is_valid()) { logger('Activity rejected: ' . print_r($data, true)); return; diff --git a/Zotlabs/Module/Activity.php b/Zotlabs/Module/Activity.php index b2f0ce498..133312e28 100644 --- a/Zotlabs/Module/Activity.php +++ b/Zotlabs/Module/Activity.php @@ -25,7 +25,7 @@ class Activity extends Controller { $portable_id = EMPTY_STR; - $item_normal_extra = sprintf(" and not verb in ('%s', '%s') ", + $item_normal_extra = sprintf(" and not verb in ('Follow', 'Ignore', '%s', '%s') ", dbesc(ACTIVITY_FOLLOW), dbesc(ACTIVITY_UNFOLLOW) ); @@ -186,7 +186,7 @@ class Activity extends Controller { } } - $item_normal_extra = sprintf(" and not verb in ('%s', '%s') ", + $item_normal_extra = sprintf(" and not verb in ('Follow', 'Ignore', '%s', '%s') ", dbesc(ACTIVITY_FOLLOW), dbesc(ACTIVITY_UNFOLLOW) ); diff --git a/Zotlabs/Module/Contactedit.php b/Zotlabs/Module/Contactedit.php index e20e90872..3527e9380 100644 --- a/Zotlabs/Module/Contactedit.php +++ b/Zotlabs/Module/Contactedit.php @@ -177,22 +177,8 @@ class Contactedit extends Controller { intval($channel['channel_id']) ); if (($pr) && (!intval($contact['abook_hidden'])) && (intval(get_pconfig($channel['channel_id'], 'system', 'post_newfriend')))) { - $xarr = []; - - $xarr['item_wall'] = 1; - $xarr['item_origin'] = 1; - $xarr['item_thread_top'] = 1; - $xarr['owner_xchan'] = $xarr['author_xchan'] = $channel['channel_hash']; - $xarr['allow_cid'] = $channel['channel_allow_cid']; - $xarr['allow_gid'] = $channel['channel_allow_gid']; - $xarr['deny_cid'] = $channel['channel_deny_cid']; - $xarr['deny_gid'] = $channel['channel_deny_gid']; - $xarr['item_private'] = (($xarr['allow_cid'] || $xarr['allow_gid'] || $xarr['deny_cid'] || $xarr['deny_gid']) ? 1 : 0); - $xarr['body'] = '[zrl=' . $channel['xchan_url'] . ']' . $channel['xchan_name'] . '[/zrl]' . ' ' . t('is now connected to') . ' ' . '[zrl=' . $contact['xchan_url'] . ']' . $contact['xchan_name'] . '[/zrl]'; - - $xarr['body'] .= "\n\n\n" . '[zrl=' . $contact['xchan_url'] . '][zmg=80x80]' . $contact['xchan_photo_m'] . '[/zmg][/zrl]'; - + $xarr['body'] .= "\n\n\n" . '[zrl=' . $contact['xchan_url'] . '][zmg=' . $contact['xchan_photo_m'] . ']' . $contact['xchan_name'] . '[/zmg][/zrl]'; post_activity_item($xarr); } diff --git a/Zotlabs/Module/Conversation.php b/Zotlabs/Module/Conversation.php index 86ce66caa..aa8349f55 100644 --- a/Zotlabs/Module/Conversation.php +++ b/Zotlabs/Module/Conversation.php @@ -25,7 +25,7 @@ class Conversation extends Controller { $portable_id = EMPTY_STR; - $item_normal_extra = sprintf(" and not verb in ('%s', '%s') ", + $item_normal_extra = sprintf(" and not verb in ('Follow', 'Ignore', '%s', '%s') ", dbesc(ACTIVITY_FOLLOW), dbesc(ACTIVITY_UNFOLLOW) ); diff --git a/Zotlabs/Module/Cover_photo.php b/Zotlabs/Module/Cover_photo.php index 1ecbfce3e..f4f9480c0 100644 --- a/Zotlabs/Module/Cover_photo.php +++ b/Zotlabs/Module/Cover_photo.php @@ -93,8 +93,6 @@ class Cover_photo extends \Zotlabs\Web\Controller { $image_id = substr($image_id,0,-2); } - - $srcX = intval($_POST['xstart']); $srcY = intval($_POST['ystart']); $srcW = intval($_POST['xfinal']) - $srcX; @@ -228,7 +226,7 @@ class Cover_photo extends \Zotlabs\Web\Controller { return; } - $this->send_cover_photo_activity($channel,$base_image,$profile); + profile_activity([t('Cover Photo')], $base_image['resource_id']); $sync = attach_export_data($channel,$base_image['resource_id']); if($sync) @@ -245,7 +243,6 @@ class Cover_photo extends \Zotlabs\Web\Controller { } - $hash = photo_new_resource(); $smallest = 0; @@ -287,45 +284,6 @@ class Cover_photo extends \Zotlabs\Web\Controller { } - function send_cover_photo_activity($channel,$photo,$profile) { - - $arr = array(); - $arr['item_thread_top'] = 1; - $arr['item_origin'] = 1; - $arr['item_wall'] = 1; - - if($profile && stripos($profile['gender'],t('female')) !== false) - $t = t('%1$s updated her %2$s'); - elseif($profile && stripos($profile['gender'],t('male')) !== false) - $t = t('%1$s updated his %2$s'); - else - $t = t('%1$s updated their %2$s'); - - $ptext = '[zrl=' . z_root() . '/photos/' . $channel['channel_address'] . '/image/' . $photo['resource_id'] . ']' . t('cover photo') . '[/zrl]'; - - $ltext = '[zrl=' . z_root() . '/profile/' . $channel['channel_address'] . ']' . '[zmg]' . z_root() . '/photo/' . $photo['resource_id'] . '-8[/zmg][/zrl]'; - - $arr['body'] = sprintf($t,$channel['channel_name'],$ptext) . "\n\n" . $ltext; - - $acl = new \Zotlabs\Access\AccessList($channel); - $x = $acl->get(); - $arr['allow_cid'] = $x['allow_cid']; - - $arr['allow_gid'] = $x['allow_gid']; - $arr['deny_cid'] = $x['deny_cid']; - $arr['deny_gid'] = $x['deny_gid']; - - $arr['uid'] = $channel['channel_id']; - $arr['aid'] = $channel['channel_account_id']; - - $arr['owner_xchan'] = $channel['channel_hash']; - $arr['author_xchan'] = $channel['channel_hash']; - - post_activity_item($arr); - - - } - /** * @brief Generate content of profile-photo view @@ -334,7 +292,6 @@ class Cover_photo extends \Zotlabs\Web\Controller { * */ - function get() { if(! local_channel()) { diff --git a/Zotlabs/Module/Item.php b/Zotlabs/Module/Item.php index 7d71edc99..9c967cb88 100644 --- a/Zotlabs/Module/Item.php +++ b/Zotlabs/Module/Item.php @@ -52,7 +52,7 @@ class Item extends Controller { $portable_id = EMPTY_STR; - $item_normal_extra = sprintf(" and not verb in ('%s', '%s') ", + $item_normal_extra = sprintf(" and not verb in ('Follow', 'Ignore', '%s', '%s') ", dbesc(ACTIVITY_FOLLOW), dbesc(ACTIVITY_UNFOLLOW) ); @@ -168,7 +168,7 @@ class Item extends Controller { $portable_id = EMPTY_STR; - $item_normal_extra = sprintf(" and not verb in ('%s', '%s') ", + $item_normal_extra = sprintf(" and not verb in ('Follow', 'Ignore', '%s', '%s') ", dbesc(ACTIVITY_FOLLOW), dbesc(ACTIVITY_UNFOLLOW) ); diff --git a/Zotlabs/Module/Like.php b/Zotlabs/Module/Like.php index 4c9828b2c..86ae48365 100644 --- a/Zotlabs/Module/Like.php +++ b/Zotlabs/Module/Like.php @@ -322,6 +322,8 @@ class Like extends Controller { // parent, copy that as well. if ($r) { + $obj_type = $r[0]['obj_type']; + if ($r[0]['uid'] === $sys_channel['channel_id'] && local_channel()) { $r = [copy_of_pubitem(App::get_channel(), $r[0]['mid'])]; } @@ -432,7 +434,7 @@ class Like extends Controller { } } - $uuid = item_message_id(); + $uuid = new_uuid(); $arr = array(); @@ -445,17 +447,17 @@ class Like extends Controller { $arr['item_wall'] = 1; } else { - switch ($item['resource_type']) { - case 'photo': - $obj_type = 'Image'; - $post_type = t('photo'); + switch ($item['object_type']) { + case 'Image': + $post_type = t('image'); break; - case 'event': - $obj_type = 'Invite'; + case 'Invite': $post_type = t('event'); break; + case 'Profile': + $post_type = t('profile'); + break; default: - $obj_type = 'Note'; $post_type = t('status'); break; } @@ -527,7 +529,7 @@ class Like extends Controller { if ($obj_type === 'thing' && $r[0]['imgurl']) { $arr['body'] .= "\n\n[zmg=80x80]" . $r[0]['imgurl'] . '[/zmg]'; } - if ($obj_type === 'profile') { + if ($obj_type === 'Profile') { if ($public) { $arr['body'] .= "\n\n" . '[embed]' . z_root() . '/profile/' . $ch[0]['channel_address'] . '[/embed]'; } @@ -581,6 +583,7 @@ class Like extends Controller { Libsync::build_sync_packet($profile_uid, ['item' => [encode_item($sync_item[0], true)]]); } + if ($extended_like) { $r = q("insert into likes (channel_id,liker,likee,iid,i_mid,verb,target_type,target_id,target) values (%d,'%s','%s',%d,'%s','%s','%s','%s','%s')", intval($ch[0]['channel_id']), diff --git a/Zotlabs/Module/Profile_photo.php b/Zotlabs/Module/Profile_photo.php index d7e2bbce1..dc47d213b 100644 --- a/Zotlabs/Module/Profile_photo.php +++ b/Zotlabs/Module/Profile_photo.php @@ -223,7 +223,7 @@ class Profile_photo extends Controller { intval(local_channel()) ); - send_profile_photo_activity($channel, $base_image, $profile); + profile_activity([t('Profile Photo')], $base_image['resource_id']); } else { q("update profile set photo = '%s', thumb = '%s' where id = %d and uid = %d", @@ -269,7 +269,6 @@ class Profile_photo extends Controller { // Update directory in background Master::Summon(['Directory', $channel['channel_id']]); - } else notice(t('Unable to process image') . EOL); diff --git a/Zotlabs/Module/Profiles.php b/Zotlabs/Module/Profiles.php index ce496252b..15252d6e6 100644 --- a/Zotlabs/Module/Profiles.php +++ b/Zotlabs/Module/Profiles.php @@ -3,10 +3,6 @@ namespace Zotlabs\Module; use Zotlabs\Lib\Libsync; -require_once('include/channel.php'); -require_once('include/selectors.php'); - - class Profiles extends \Zotlabs\Web\Controller { function init() { @@ -492,7 +488,7 @@ class Profiles extends \Zotlabs\Web\Controller { $publish = ((x($_POST, 'profile_in_directory') && (intval($_POST['profile_in_directory']) == 1)) ? 1 : 0); - profile_activity($changes,$value); + profile_activity($changes, $value); } diff --git a/Zotlabs/Module/Sse_bs.php b/Zotlabs/Module/Sse_bs.php index a621f3608..71fc16aae 100644 --- a/Zotlabs/Module/Sse_bs.php +++ b/Zotlabs/Module/Sse_bs.php @@ -194,7 +194,7 @@ class Sse_bs extends Controller { $item_normal = item_normal(); // Filter internal follow activities and strerams add/remove activities - $item_normal .= " AND verb NOT IN ('Add', 'Remove', '" . dbesc(ACTIVITY_FOLLOW) . "') "; + $item_normal .= " AND verb NOT IN ('Add', 'Remove', 'Follow', 'Ignore', '" . dbesc(ACTIVITY_FOLLOW) . "') "; if ($notifications) { $items = q("SELECT * FROM item @@ -277,7 +277,7 @@ class Sse_bs extends Controller { $item_normal = item_normal(); // Filter internal follow activities and strerams add/remove activities - $item_normal .= " AND verb NOT IN ('Add', 'Remove', '" . dbesc(ACTIVITY_FOLLOW) . "') "; + $item_normal .= " AND verb NOT IN ('Add', 'Remove', 'Follow', 'Ignore', '" . dbesc(ACTIVITY_FOLLOW) . "') "; if ($notifications) { $items = q("SELECT * FROM item @@ -360,7 +360,7 @@ class Sse_bs extends Controller { $item_normal = item_normal(); // Filter internal follow activities and strerams add/remove activities - $item_normal .= " AND verb NOT IN ('Add', 'Remove', '" . dbesc(ACTIVITY_FOLLOW) . "') "; + $item_normal .= " AND verb NOT IN ('Add', 'Remove', 'Follow', 'Ignore', '" . dbesc(ACTIVITY_FOLLOW) . "') "; if ($notifications) { $items = q("SELECT * FROM item @@ -467,7 +467,7 @@ class Sse_bs extends Controller { $item_normal = item_normal(); // Filter internal follow activities and strerams add/remove activities - $item_normal .= " AND verb NOT IN ('Add', 'Remove', '" . dbesc(ACTIVITY_FOLLOW) . "') "; + $item_normal .= " AND verb NOT IN ('Add', 'Remove', 'Follow', 'Ignore', '" . dbesc(ACTIVITY_FOLLOW) . "') "; if ($notifications) { $items = q("SELECT * FROM item @@ -663,7 +663,7 @@ class Sse_bs extends Controller { $item_normal = item_normal(); // Filter internal follow activities and strerams add/remove activities - $item_normal .= " AND verb NOT IN ('Add', 'Remove', '" . dbesc(ACTIVITY_FOLLOW) . "') "; + $item_normal .= " AND verb NOT IN ('Add', 'Remove', 'Follow', 'Ignore', '" . dbesc(ACTIVITY_FOLLOW) . "') "; $r = q("SELECT * FROM item WHERE (verb = 'Create' OR verb = '%s') diff --git a/Zotlabs/Module/Subthread.php b/Zotlabs/Module/Subthread.php index 5ddcaffc6..b927ee480 100644 --- a/Zotlabs/Module/Subthread.php +++ b/Zotlabs/Module/Subthread.php @@ -24,9 +24,9 @@ class Subthread extends \Zotlabs\Web\Controller { $item_id = ((argc() > 2) ? notags(trim(argv(2))) : 0); if(argv(1) === 'sub') - $activity = ACTIVITY_FOLLOW; + $activity = 'Follow'; elseif(argv(1) === 'unsub') - $activity = ACTIVITY_UNFOLLOW; + $activity = 'Ignore'; $i = q("select * from item where id = %d and uid = %d", @@ -121,9 +121,9 @@ class Subthread extends \Zotlabs\Web\Controller { if(! intval($item['item_thread_top'])) $post_type = 'comment'; - if($activity === ACTIVITY_FOLLOW) + if($activity === 'Follow') $bodyverb = t('%1$s is following %2$s\'s %3$s'); - if($activity === ACTIVITY_UNFOLLOW) + if($activity === 'Ignore') $bodyverb = t('%1$s stopped following %2$s\'s %3$s'); $arr = array(); diff --git a/Zotlabs/Widget/Messages.php b/Zotlabs/Widget/Messages.php index 519bb27fe..8654d8e8f 100644 --- a/Zotlabs/Widget/Messages.php +++ b/Zotlabs/Widget/Messages.php @@ -62,7 +62,7 @@ class Messages { $channel = App::get_channel(); $item_normal = item_normal(); // Filter internal follow activities and strerams add/remove activities - $item_normal .= " and item.verb not in ('Add', 'Remove', '" . ACTIVITY_FOLLOW . "') "; + $item_normal .= " and item.verb not in ('Add', 'Remove', 'Follow', 'Ignore', '" . ACTIVITY_FOLLOW . "') "; $item_normal_i = str_replace('item.', 'i.', $item_normal); $item_normal_c = str_replace('item.', 'c.', $item_normal); $entries = []; @@ -85,7 +85,7 @@ class Messages { } if($author) { - $author_sql = " AND (i.owner_xchan = '" . protect_sprintf(dbesc($author)) . "' OR i.source_xchan = '" . protect_sprintf(dbesc($author)) . "') "; + $author_sql = " AND (i.owner_xchan = '" . protect_sprintf(dbesc($author)) . "') "; } switch($type) { @@ -60,6 +60,8 @@ require_once('include/bbcode.php'); require_once('include/items.php'); require_once('include/conversation.php'); require_once('include/acl_selectors.php'); +require_once('include/selectors.php'); +require_once('include/activities.php'); define('PLATFORM_NAME', 'hubzilla'); define('STD_VERSION', '8.9.9'); @@ -510,13 +512,13 @@ define('ACTIVITY_ATTENDMAYBE', NAMESPACE_ZOT . '/activity/attendmaybe'); // AS2 define('ACTIVITY_FRIEND', NAMESPACE_ACTIVITY_SCHEMA . 'make-friend'); // deprecated -define('ACTIVITY_FOLLOW', NAMESPACE_ACTIVITY_SCHEMA . 'follow'); -define('ACTIVITY_UNFOLLOW', NAMESPACE_ACTIVITY_SCHEMA . 'stop-following'); +define('ACTIVITY_FOLLOW', NAMESPACE_ACTIVITY_SCHEMA . 'follow'); // AS2 Follow +define('ACTIVITY_UNFOLLOW', NAMESPACE_ACTIVITY_SCHEMA . 'stop-following'); // AS2 Ignore define('ACTIVITY_POST', NAMESPACE_ACTIVITY_SCHEMA . 'post'); // AS2 Create -define('ACTIVITY_UPDATE', NAMESPACE_ACTIVITY_SCHEMA . 'update'); +define('ACTIVITY_UPDATE', NAMESPACE_ACTIVITY_SCHEMA . 'update'); // AS2 Update define('ACTIVITY_TAG', NAMESPACE_ACTIVITY_SCHEMA . 'tag'); // unused @@ -537,7 +539,7 @@ define('ACTIVITY_OBJ_PHOTO', NAMESPACE_ACTIVITY_SCHEMA . 'photo'); // AS2 Image define('ACTIVITY_OBJ_EVENT', NAMESPACE_ACTIVITY_SCHEMA . 'event'); // AS2 Event define('ACTIVITY_OBJ_TAGTERM', NAMESPACE_ZOT . '/activity/tagterm'); // unused -define('ACTIVITY_OBJ_PROFILE', NAMESPACE_ZOT . '/activity/profile'); // AS2 Profile (broken) +define('ACTIVITY_OBJ_PROFILE', NAMESPACE_ZOT . '/activity/profile'); // AS2 Profile define('ACTIVITY_OBJ_THING', NAMESPACE_ZOT . '/activity/thing'); // AS2 Page??? (broken) diff --git a/include/activities.php b/include/activities.php index f5f0e55da..c06a8f6c4 100644 --- a/include/activities.php +++ b/include/activities.php @@ -1,100 +1,83 @@ <?php /** @file */ -function profile_activity($changed, $value) { - - // TODO: we should probably send an Update/Profile or Person activity here. - // We could also just send a Note with some changed info? - // Profiles are currently a hubzilla specific thing. - return; +use Zotlabs\Lib\Activity; +use Zotlabs\Daemon\Master; - if(! local_channel() || ! is_array($changed) || ! count($changed)) - return; +function profile_activity($changed, $value) { - if(! get_pconfig(local_channel(),'system','post_profilechange')) + if (!local_channel() || !is_array($changed) || !count($changed)) { return; + } - require_once('include/items.php'); - - $self = App::get_channel(); - - if(! count($self)) + if (!get_pconfig(local_channel(), 'system', 'post_profilechange')) { return; + } - $arr = array(); - $arr['uuid'] = item_message_id(); - $arr['mid'] = $arr['parent_mid'] = z_root() . '/item/' . $arr['uuid']; - $arr['uid'] = local_channel(); - $arr['aid'] = $self['channel_account_id']; - $arr['owner_xchan'] = $arr['author_xchan'] = $self['xchan_hash']; - - $arr['item_wall'] = 1; - $arr['item_origin'] = 1; - $arr['item_thread_top'] = 1; - $arr['verb'] = ACTIVITY_UPDATE; - $arr['obj_type'] = 'Profile'; - - $arr['plink'] = z_root() . '/channel/' . $self['channel_address'] . '/?f=&mid=' . urlencode($arr['mid']); + $channel = App::get_channel(); - $A = '[url=' . z_root() . '/channel/' . $self['channel_address'] . ']' . $self['channel_name'] . '[/url]'; + $arr['verb'] = 'Update'; + $arr['obj_type'] = 'Profile'; + $channel_link = '[url=' . z_root() . '/channel/' . $channel['channel_address'] . ']' . $channel['channel_name'] . '[/url]'; $changes = ''; $t = count($changed); $z = 0; - foreach($changed as $ch) { - if(strlen($changes)) { - if ($z == ($t - 1)) + $photo = false; + foreach ($changed as $ch) { + if (strlen($changes)) { + if ($z == ($t - 1)) { $changes .= t(' and '); - else - $changes .= ', '; + } else { + $changes .= t(', '); + } } - $z ++; + + if (in_array($ch, [t('Profile Photo'), t('Cover Photo')])) { + $photo = true; + $photo_size = (($ch === t('Profile Photo')) ? 4 : 8); + } + + $z++; $changes .= $ch; } - $prof = '[url=' . z_root() . '/profile/' . $self['channel_address'] . ']' . t('public profile') . '[/url]'; + $profile_link = '[url=' . z_root() . '/profile/' . $channel['channel_address'] . ']' . t('public profile') . '[/url]'; - if($t == 1 && strlen($value)) { + if ($t == 1 && strlen($value)) { // if it's a url, the HTML quotes will mess it up, so link it and don't try and zidify it because we don't know what it points to. - $value = preg_replace_callback("/([^\]\='".'"'."]|^|\#\^)(https?\:\/\/[a-zA-Z0-9\pL\:\/\-\?\&\;\.\=\@\_\~\#\%\$\!\+\,]+)/ismu", 'red_zrl_callback', $value); + $value = preg_replace_callback("/([^='" . '"' . "]|^|#\^)(https?:\/\/[a-zA-Z0-9\pL:\/\-?&;.=@_~#%\$!+,]+)/ismu", 'red_zrl_callback', $value); // take out the bookmark indicator - if(substr($value,0,2) === '#^') - $value = str_replace('#^','',$value); - - $message = sprintf( t('%1$s changed %2$s to “%3$s”'), $A, $changes, $value); - $message .= "\n\n" . sprintf( t('Visit %1$s\'s %2$s'), $A, $prof); - } - else - $message = sprintf( t('%1$s has an updated %2$s, changing %3$s.'), $A, $prof, $changes); - - - $arr['body'] = $message; - - $links = array(); - $links[] = array('rel' => 'alternate', 'type' => 'text/html', - 'href' => z_root() . '/profile/' . $self['channel_address']); - $links[] = array('rel' => 'photo', 'type' => $self['xchan_photo_mimetype'], - 'href' => $self['xchan_photo_l']); + if (str_starts_with($value, '#^')) { + $value = str_replace('#^', '', $value); + } - $arr['object'] = json_encode(array( - 'type' => 'Profile', - 'title' => $self['channel_name'], - 'id' => $self['xchan_url'] . '/' . $self['xchan_hash'], - 'link' => $links - )); + if ($photo) { + $value = "\n\n" . '[zmg=' . z_root() . '/photo/' . $value . '-' . $photo_size . ']' . $ch . ' ' . $channel['xchan_name'] . '[/zmg]'; + } + else { + $value = '"' . $value . '"'; + } + $message = sprintf(t('%1$s %2$s has been updated to %3$s.'), $channel_link . '\'s' . (($photo) ? '' : ' ' . $profile_link), strtolower($changes), $value); - $arr['allow_cid'] = $self['channel_allow_cid']; - $arr['allow_gid'] = $self['channel_allow_gid']; - $arr['deny_cid'] = $self['channel_deny_cid']; - $arr['deny_gid'] = $self['channel_deny_gid']; + } else { + $message = sprintf(t('%1$s updated the %2$s. Changed %3$s.'), $channel_link, $profile_link, strtolower($changes)); + } - $res = item_store($arr); - $i = $res['item_id']; + $arr['body'] = $message; - if($i) { - // FIXME - limit delivery in notifier.php to those specificed in the perms argument - Zotlabs\Daemon\Master::Summon(array('Notifier','activity', $i, 'PERMS_R_PROFILE')); - } + $arr['obj'] = [ + 'type' => 'Profile', + 'content' => bbcode($message), + 'source' => [ + 'content' => $message, + 'mediaType' => 'text/bbcode' + ], + 'describes' => Activity::encode_person($channel), + 'url' => z_root() . '/profile/' . $channel['channel_address'] + ]; + + post_activity_item($arr); } diff --git a/include/channel.php b/include/channel.php index b8affa3ca..a82794bfd 100644 --- a/include/channel.php +++ b/include/channel.php @@ -1771,6 +1771,7 @@ function advanced_profile() { if(App::$profile['gender']) $profile['gender'] = array( t('Gender:'), App::$profile['gender'] ); $ob_hash = get_observer_hash(); +/* TODO: AS2 compatibility if($ob_hash && perm_is_allowed(App::$profile['profile_uid'],$ob_hash,'post_like')) { $profile['canlike'] = true; $profile['likethis'] = t('Like this channel'); @@ -1790,7 +1791,7 @@ function advanced_profile() { foreach($likers as $l) $profile['likers'][] = array('name' => $l['xchan_name'],'photo' => zid($l['xchan_photo_s']), 'url' => zid($l['xchan_url'])); } - +*/ if((App::$profile['dob']) && (App::$profile['dob'] != '0000-00-00')) { $val = ''; diff --git a/include/contact_widgets.php b/include/contact_widgets.php index 182f674ca..c05ecaf7c 100644 --- a/include/contact_widgets.php +++ b/include/contact_widgets.php @@ -85,7 +85,7 @@ function categories_widget($baseurl,$selected = '') { AND term.otype = %d AND item.owner_xchan = '%s' AND item.item_wall = 1 - AND item.verb != '%s' + AND item.verb NOT IN ('Update', '%s') $item_normal $sql_extra ORDER BY term.term ASC", diff --git a/include/dba/dba_transaction.php b/include/dba/dba_transaction.php new file mode 100644 index 000000000..02e9945ca --- /dev/null +++ b/include/dba/dba_transaction.php @@ -0,0 +1,64 @@ +<?php +/** + * Class to represent a database transaction. + * + * A database transaction is initiated upon construction of an object of this + * class. The transaction will be automatically rolled back upon destruction + * unless it has been explicitly committed by calling the `commit` method. + * + * Wrapping multiple database operation within a transaction ensures that all + * (or none) of the operations are successfully completed at the same time. + * + * If a transaction is already active when constructing an object of this + * class, it will _not_ try to initiate a transaction, but constructs an object + * that will in practice be a stub. This prevents that "nested" transactions + * will cause problems with the existing active transaction. + * + * It also means that any rollbacks or commits perfomed on the "nested" + * transaction will be ignored, and postponed to the outer transaction is + * committed or rolled back. + * + * Also note that any modification to the database schema will implicitly + * commit active transactions in most cases, so be careful about relying on + * transactions in those cases. + * + * @Note This class assumes the actual underlying database driver is PDO. + */ +class DbaTransaction { + private bool $committed = false; + private bool $active = false; + + /** + * Creates a database transaction object. + * + * If a transaction is already active for this db connection, + * no transaction is initiated, and the constructed object will + * not perform any commit or rollback actions. + */ + public function __construct(private dba_driver $dba) { + if (! $this->dba->db->inTransaction()) { + $this->active = $this->dba->db->beginTransaction(); + } + } + + /** + * Roll back the transaction if it is active and not already committed. + */ + public function __destruct() { + if ($this->active && ! $this->committed) { + $this->dba->db->rollBack(); + } + } + + /** + * Commit the transaction if active. + * + * This will also mark the transaction as committed, preventing it from + * being attempted rolled back on destruction. + */ + public function commit(): void { + if ($this->active && ! $this->committed) { + $this->committed = $this->dba->db->commit(); + } + } +} diff --git a/include/items.php b/include/items.php index 8a0af5679..e26366af5 100644 --- a/include/items.php +++ b/include/items.php @@ -459,7 +459,7 @@ function post_activity_item($arr, $allow_code = false, $deliver = true) { if(! $arr['mid']) { - $arr['uuid'] = ((x($arr,'uuid')) ? $arr['uuid'] : item_message_id()); + $arr['uuid'] = ((x($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']); @@ -520,7 +520,7 @@ function post_activity_item($arr, $allow_code = false, $deliver = true) { return $ret; if($post_id && $deliver) { - Master::Summon([ 'Notifier','activity',$post_id ]); + Master::Summon(['Notifier','activity', $post_id]); } $ret['success'] = true; @@ -2487,7 +2487,7 @@ function send_status_notifications($post_id,$item) { // check for an unfollow thread activity - we should probably decode the obj and check the id // but it will be extremely rare for this to be wrong. - if(($xx['verb'] === ACTIVITY_UNFOLLOW) + if((in_array($xx['verb'], ['Ignore', ACTIVITY_UNFOLLOW])) && (in_array($xx['obj_type'], ['Note', 'Image', ACTIVITY_OBJ_NOTE, ACTIVITY_OBJ_PHOTO])) && ($xx['parent'] != $xx['id'])) $unfollowed = true; @@ -2514,7 +2514,6 @@ function send_status_notifications($post_id,$item) { if(! $notify) return; - Enotify::submit(array( 'type' => $type, 'from_xchan' => $item['author_xchan'], @@ -4377,7 +4376,7 @@ function items_fetch($arr,$channel = null,$observer_hash = null,$client_mode = C $item_normal = item_normal(); if (! (isset($arr['include_follow']) && intval($arr['include_follow']))) { - $item_normal .= sprintf(" and not verb in ('%s', '%s') ", + $item_normal .= sprintf(" and not verb in ('Follow', 'Ignore', '%s', '%s') ", dbesc(ACTIVITY_FOLLOW), dbesc(ACTIVITY_UNFOLLOW) ); @@ -4790,54 +4789,7 @@ function comment_local_origin($item) { return false; } - - -function send_profile_photo_activity($channel,$photo,$profile) { - - // for now only create activities for the default profile - - if(! intval($profile['is_default'])) - return; - - $arr = array(); - $arr['item_thread_top'] = 1; - $arr['item_origin'] = 1; - $arr['item_wall'] = 1; - - if(stripos($profile['gender'],t('female')) !== false) - $t = t('%1$s updated her %2$s'); - elseif(stripos($profile['gender'],t('male')) !== false) - $t = t('%1$s updated his %2$s'); - else - $t = t('%1$s updated their %2$s'); - - $ptext = '[zrl=' . z_root() . '/photos/' . $channel['channel_address'] . '/image/' . $photo['resource_id'] . ']' . t('profile photo') . '[/zrl]'; - - $ltext = '[zrl=' . z_root() . '/profile/' . $channel['channel_address'] . ']' . '[zmg=150x150]' . z_root() . '/photo/' . $photo['resource_id'] . '-4[/zmg][/zrl]'; - - $arr['body'] = sprintf($t,$channel['channel_name'],$ptext) . "\n\n" . $ltext; - - $acl = new Zotlabs\Access\AccessList($channel); - $x = $acl->get(); - - $arr['allow_cid'] = $x['allow_cid']; - - $arr['allow_gid'] = $x['allow_gid']; - $arr['deny_cid'] = $x['deny_cid']; - $arr['deny_gid'] = $x['deny_gid']; - - $arr['uid'] = $channel['channel_id']; - $arr['aid'] = $channel['channel_account_id']; - - $arr['owner_xchan'] = $channel['channel_hash']; - $arr['author_xchan'] = $channel['channel_hash']; - - post_activity_item($arr); -} - - function sync_an_item($channel_id,$item_id) { - $r = q("select * from item where id = %d", intval($item_id) ); diff --git a/include/photo/photo_driver.php b/include/photo/photo_driver.php index 522e638de..4394d3238 100644 --- a/include/photo/photo_driver.php +++ b/include/photo/photo_driver.php @@ -117,7 +117,14 @@ function guess_image_type($filename, $data = '') { $body = $data['body']; if ($body) { $image = new Imagick(); - $image->readImageBlob($body); + + try{ + $image->readImageBlob($body); + } catch (\Exception $e) { + logger('Imagick readImageBlob() exception:' . print_r($e, true)); + return $type; + } + $r = $image->identifyImage(); if ($r && is_array($r) && array_key_exists($r['mimetype'], $types)) $type = $r['mimetype']; diff --git a/include/taxonomy.php b/include/taxonomy.php index cfec8414a..90ccb6142 100644 --- a/include/taxonomy.php +++ b/include/taxonomy.php @@ -58,7 +58,7 @@ function term_item_parent_query($uid,$table,$s,$type = TERM_UNKNOWN, $type2 = '' $s = str_replace('*','%',$s); if($type2) { - $r = q("select parent from item left join term on term.oid = item.id where term.ttype in (%d, %d) and term.term like '%s' and term.uid = %d and term.otype = 1 and item.verb != '%s'", + $r = q("select parent from item left join term on term.oid = item.id where term.ttype in (%d, %d) and term.term like '%s' and term.uid = %d and term.otype = 1 and item.verb NOT IN ('Update', '%s')", intval($type), intval($type2), dbesc($s), @@ -67,7 +67,7 @@ function term_item_parent_query($uid,$table,$s,$type = TERM_UNKNOWN, $type2 = '' ); } else { - $r = q("select parent from item left join term on term.oid = item.id where term.ttype = %d and term.term like '%s' and term.uid = %d and term.otype = 1 and item.verb != '%s'", + $r = q("select parent from item left join term on term.oid = item.id where term.ttype = %d and term.term like '%s' and term.uid = %d and term.otype = 1 and item.verb NOT IN ('Update', '%s')", intval($type), dbesc($s), intval($uid), diff --git a/include/text.php b/include/text.php index d6256b75b..aa9650a25 100644 --- a/include/text.php +++ b/include/text.php @@ -1750,7 +1750,7 @@ function prepare_body(&$item,$attach = false,$opts = false) { } - $poll = (($item['obj_type'] === 'Question' && in_array($item['verb'],['Create', ACTIVITY_POST, ACTIVITY_UPDATE, ACTIVITY_SHARE])) ? format_poll($item, $s, $opts) : false); + $poll = (($item['obj_type'] === 'Question' && in_array($item['verb'],['Create', 'Update', ACTIVITY_POST, ACTIVITY_UPDATE, ACTIVITY_SHARE])) ? format_poll($item, $s, $opts) : false); if ($poll) { $s = $poll; } diff --git a/tests/fakes/fake_dba.php b/tests/fakes/fake_dba.php new file mode 100644 index 000000000..2289f5c80 --- /dev/null +++ b/tests/fakes/fake_dba.php @@ -0,0 +1,18 @@ +<?php +namespace Zotlabs\Tests\Fakes; + +require_once 'include/dba/dba_pdo.php'; + +/** + * Fake dba_driver implementation. + * + * This is a subclass of the dba_pdo class, that essentially lets us inject a + * stub for the PDO class that is the actual database driver. + */ +class FakeDba extends \dba_pdo { + public function __construct($stub) { + $this->db = $stub; + $this->connected = true; + } +} + diff --git a/tests/unit/UnitTestCase.php b/tests/unit/UnitTestCase.php index 0bf7b547a..18467d91e 100644 --- a/tests/unit/UnitTestCase.php +++ b/tests/unit/UnitTestCase.php @@ -23,6 +23,7 @@ namespace Zotlabs\Tests\Unit; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\TestResult; /* * Make sure global constants and the global App object is available to the @@ -41,10 +42,39 @@ require_once 'include/dba/dba_driver.php' ; * @author Klaus Weidenbach */ class UnitTestCase extends TestCase { - private bool $in_transaction = false; protected array $fixtures = array(); - public static function setUpBeforeClass() : void { + /** + * Override the PHPUnit\Framework\TestCase::run method, so we can + * wrap it in a database transaction. + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + public function run(TestResult $result = null): TestResult { + // $myclass = get_class($this); + // logger("[*] Running test: {$myclass}::{$this->getName(true)}", LOGGER_DEBUG); + + if (! \DBA::$dba) { + //logger('[*] Connecting to test db...'); + $this->connect_to_test_db(); + } + + // The $transactuion variable is needed to hold the transaction until the + // function returns. + $transaction = new \DbaTransaction(\DBA::$dba); + + $this->loadFixtures(); + + // Make sure app config is reset and loaded from fixtures + \App::$config = array(); + \Zotlabs\Lib\Config::Load('system'); + + $result = parent::run($result); + + return $result; + } + + protected function connect_to_test_db() : void { if ( !\DBA::$dba ) { \DBA::dba_factory( getenv('HZ_TEST_DB_HOST') ?: 'db', @@ -71,36 +101,6 @@ class UnitTestCase extends TestCase { } } - protected function setUp() : void { - $myclass = get_class($this); - logger("[*] Running test: {$myclass}::{$this->getName(true)}", LOGGER_DEBUG); - if ( \DBA::$dba->connected ) { - // Create a transaction, so that any actions taken by the - // tests does not change the actual contents of the database. - $this->in_transaction = \DBA::$dba->db->beginTransaction(); - - $this->loadFixtures(); - } - - // Make sure app config is reset and loaded from fixtures - \App::$config = array(); - \Zotlabs\Lib\Config::Load('system'); - } - - protected function tearDown() : void { - if ( \DBA::$dba->connected && $this->in_transaction ) { - // Roll back the transaction, restoring the db to the - // state it was before the test was run. - if ( \DBA::$dba->db->rollBack() ) { - $this->in_transaction = false; - } else { - throw new \Exception( - "Transaction rollback failed! Error is: " - . \DBA::$dba->db->errorInfo()); - } - } - } - private static function dbtype(string $type): int { if (trim(strtolower($type)) === 'postgres') { return DBTYPE_POSTGRES; diff --git a/tests/unit/includes/dba/TransactionTest.php b/tests/unit/includes/dba/TransactionTest.php new file mode 100644 index 000000000..99e3f459d --- /dev/null +++ b/tests/unit/includes/dba/TransactionTest.php @@ -0,0 +1,207 @@ +<?php +/* + * Copyright (c) 2024 Hubzilla + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +require_once 'tests/fakes/fake_dba.php'; +require_once 'include/dba/dba_transaction.php'; + +use \PHPUnit\Framework\TestCase; +use \Zotlabs\Tests\Fakes\FakeDba; + +/** + * Test database transactions. + * + * This class subclass the base PHPUnit TestCase class, since we do _not_ + * want a real database connection for these tests. We're testing functionality + * of the database adapter itself, so we choose to stub the underlying db driver + * to be able to assert that the adapter behaves as it should. + */ +class DbaTransactionTest extends TestCase { + private $pdo_stub; + + public function setUp(): void { + $this->pdo_stub = $this->createStub(PDO::class); + } + + + /** + * Test that creating a DbaTransaction object initiates a database transaction. + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + public function test_transaction_initialized_on_construction(): void { + // Stub PDO::inTransaction() + // Expect that it's called once, and return false to simulate that no + // transactions are active. + $this->pdo_stub + ->expects($this->once()) + ->method('inTransaction') + ->willReturn(false); + + // Stub PDO::beginTransaction to ensure that it is being called. + $this->pdo_stub + ->expects($this->once()) + ->method('beginTransaction') + ->willReturn(true); + + $dba = new FakeDba($this->pdo_stub); + + $transaction = new DbaTransaction($dba); + } + + /** + * Test that a transaction is rolled back when the DbaTransaction object + * is destroyed. + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + public function test_uncommitted_transaction_is_rolled_back_on_destruction(): void { + // Stub PDO::inTransaction() + // Expect that it's called once, and return false to simulate that no + // transactions are active. + $this->pdo_stub + ->expects($this->once()) + ->method('inTransaction') + ->willReturn(false); + + // Stub PDO::beginTransaction to ensure that it is being called. + $this->pdo_stub + ->expects($this->once()) + ->method('beginTransaction') + ->willReturn(true); + + // Stub PDO::rollBack to make sure we test it is being called. + $this->pdo_stub + ->expects($this->once()) + ->method('rollBack') + ->willReturn(true); + + $dba = new FakeDba($this->pdo_stub); + + $transaction = new DbaTransaction($dba); + } + + /** + * Test that a committed transaction is not rolled back when the + * DbaTransaction object goes out of scope. + */ + public function test_committed_transaction_is_not_rolled_back(): void { + // Stub PDO::inTransaction() + // Return false to simulate that no transaction is active when called. + $this->pdo_stub + ->expects($this->once()) + ->method('inTransaction') + ->willReturn(false); + + // Stub PDO::beginTransaction to ensure that it is being called. + $this->pdo_stub + ->expects($this->once()) + ->method('beginTransaction') + ->willReturn(true); + + // Stub PDO::rollBack to ensure it is _not_ called + $this->pdo_stub + ->expects($this->never()) + ->method('rollBack'); + + // Stub PDO::commit to make the test check that it is being called + $this->pdo_stub + ->expects($this->once()) + ->method('commit') + ->willReturn(true); + + $dba = new FakeDba($this->pdo_stub); + + $transaction = new DbaTransaction($dba); + $transaction->commit(); + } + + /** + * Test that commiting a transaction more than once is a no-op. + */ + public function test_that_committing_an_already_committed_transaction_does_nothing(): void { + // Stub PDO::inTransaction() + // Return false to simulate that no transaction is active when called. + $this->pdo_stub + ->expects($this->once()) + ->method('inTransaction') + ->willReturn(false); + + // Stub PDO::beginTransaction to ensure that it is being called. + $this->pdo_stub + ->expects($this->once()) + ->method('beginTransaction') + ->willReturn(true); + + // Stub PDO::rollBack to ensure it is _not_ called + $this->pdo_stub + ->expects($this->never()) + ->method('rollBack'); + + // Stub PDO::commit to make the test check that it is being called + $this->pdo_stub + ->expects($this->once()) + ->method('commit') + ->willReturn(true); + + $dba = new FakeDba($this->pdo_stub); + + $transaction = new DbaTransaction($dba); + $transaction->commit(); + $transaction->commit(); + } + + /** + * Test simulating constructing a DbaTransaction object when a transaction + * is already active. + * + * This should _not_ initiate an actual DB transaction, not call the rollBack + * method on destruction. + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + public function test_that_nesting_a_transaction_does_not_create_a_new_transaction_in_db(): void { + // Stub PDO::inTransaction() + // We simulate that a transaction is already active, by returning true from + // this method. + $this->pdo_stub + ->expects($this->once()) + ->method('inTransaction') + ->willReturn(true); + + // Stub PDO::beginTransaction + // Since a transaction is already active, we should _not_ initiate + // a new transaction when the DbaTransaction object is constructed. + $this->pdo_stub + ->expects($this->never()) + ->method('beginTransaction'); + + // Stub PDO::rollBack to ensure it is _not_ called + $this->pdo_stub + ->expects($this->never()) + ->method('rollBack'); + + $dba = new FakeDba($this->pdo_stub); + + $transaction = new DbaTransaction($dba); + } +} diff --git a/util/Doxyfile b/util/Doxyfile index 14464df81..10c43e46d 100755 --- a/util/Doxyfile +++ b/util/Doxyfile @@ -1,6 +1,7 @@ INPUT = README.md index.php boot.php include/ install/ util/ view/ Zotlabs/ RECURSIVE = YES -PROJECT_NAME = "The Hubzilla" +PROJECT_NAME = "Hubzilla" +PROJECT_BRIEF = "A powerful, privacy oriented fediverse CMS." PROJECT_LOGO = images/hz-64.png IMAGE_PATH = images/ EXCLUDE = .htconfig.php library/ doc/ store/ vendor/ .git/ util/zotsh/easywebdav/ util/generate-hooks-index/ @@ -36,4 +37,6 @@ COLLABORATION_GRAPH = NO # fix @var (https://bugzilla.gnome.org/show_bug.cgi?id=626105) # Should be obsolete with doxygen >= 1.8.15 #INPUT_FILTER = "sed -e 's/@var\s/@see /'" -INPUT_FILTER = "php util/Doxygen_phpvarfilter.php" +#INPUT_FILTER = "php util/Doxygen_phpvarfilter.php" +JAVADOC_AUTOBRIEF = YES +MARKDOWN_SUPPORT = YES |