diff options
-rw-r--r-- | .gitlab-ci.yml | 17 | ||||
-rw-r--r-- | CHANGELOG | 4 | ||||
-rw-r--r-- | Zotlabs/Lib/Activity.php | 58 | ||||
-rw-r--r-- | Zotlabs/Lib/ActivityStreams.php | 2 | ||||
-rw-r--r-- | Zotlabs/Lib/Enotify.php | 28 | ||||
-rw-r--r-- | Zotlabs/Lib/Libzot.php | 31 | ||||
-rw-r--r-- | Zotlabs/Lib/ThreadItem.php | 39 | ||||
-rw-r--r-- | Zotlabs/Module/Like.php | 4 | ||||
-rw-r--r-- | Zotlabs/Module/Share.php | 2 | ||||
-rw-r--r-- | boot.php | 2 | ||||
-rw-r--r-- | include/conversation.php | 38 | ||||
-rw-r--r-- | include/network.php | 27 | ||||
-rw-r--r-- | tests/unit/UnitTestCase.php | 2 | ||||
-rw-r--r-- | tests/unit/includes/NetworkTest.php | 89 | ||||
-rw-r--r-- | tests/unit/includes/dba/_files/config.yml | 14 | ||||
-rw-r--r-- | view/js/main.js | 2 | ||||
-rw-r--r-- | view/tpl/conv_item.tpl | 14 |
17 files changed, 242 insertions, 131 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dfaa2c082..b996bb927 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -28,10 +28,10 @@ variables: before_script: # Install & enable Xdebug for code coverage reports - apt-get update - - apt-get install -yqq libjpeg-dev libpng-dev libpq-dev libyaml-dev libzip-dev mariadb-client postgresql-client unzip zip + - apt-get install -yqq libicu-dev libjpeg-dev libpng-dev libpq-dev libyaml-dev libzip-dev mariadb-client postgresql-client unzip zip - pecl install xdebug yaml - docker-php-ext-enable xdebug yaml - - docker-php-ext-install gd bcmath pdo_mysql pdo_pgsql zip + - docker-php-ext-install gd bcmath intl pdo_mysql pdo_pgsql zip # Install composer - curl -sS https://getcomposer.org/installer | php @@ -50,11 +50,15 @@ before_script: HZ_TEST_DB_PASS: $MYSQL_ROOT_PASSWORD HZ_TEST_DB_DATABASE: $MYSQL_DATABASE script: + # Import hubzilla's DB schema - echo "USE $MYSQL_DATABASE; $(cat ./install/schema_mysql.sql)" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" + # Show databases and relations/tables of hubzilla's database - echo "SHOW DATABASES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" - echo "USE $MYSQL_DATABASE; SHOW TABLES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" + # Run the actual tests - touch dbfail.out - - vendor/bin/phpunit --configuration tests/phpunit.xml --verbose --stop-on-error --coverage-text --colors=never --log-junit tests/results/junit.xml + - vendor/bin/phpunit --configuration tests/phpunit.xml --verbose --stop-on-error --coverage-text --colors=never --log-junit tests/results/junit.xml || exit_code=$? + - if [ $exit_code -ne 0 ]; then echo "Test barfed!"; cat dbfail.out; exit $exit_code; fi coverage: '/^\s*Lines:\s*\d+.\d+\%/' @@ -74,11 +78,12 @@ before_script: # Import hubzilla's DB schema - psql -h "postgres" -U "$POSTGRES_USER" -v ON_ERROR_STOP=1 --quiet "$POSTGRES_DB" < ./install/schema_postgres.sql # Show databases and relations/tables of hubzilla's database - #- psql -h "postgres" -U "$POSTGRES_USER" -l - #- psql -h "postgres" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "\dt;" + - psql -h "postgres" -U "$POSTGRES_USER" -l + - psql -h "postgres" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "\dt;" # Run the actual tests - touch dbfail.out - - vendor/bin/phpunit --configuration tests/phpunit.xml --verbose --stop-on-error --coverage-text --colors=never --log-junit tests/results/junit.xml + - vendor/bin/phpunit --configuration tests/phpunit.xml --verbose --stop-on-error --coverage-text --colors=never --log-junit tests/results/junit.xml || exit_code=$? + - if [ $exit_code -ne 0 ]; then echo "Test barfed!"; cat dbfail.out; exit $exit_code; fi coverage: '/^\s*Lines:\s*\d+.\d+\%/' # hidden job definition with artifacts config template @@ -1,3 +1,7 @@ +Hubzilla 8.8.7 (2024-01-19) + - Fix regression in Activity::actor_store() + + Hubzilla 8.8.6 (2024-01-11) - Provide more builtin jsonld files - Development branch compatibility in Libsync diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index ab5130ada..2bf8543b2 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -59,13 +59,16 @@ class Activity { "select *, id as item_id from item where mid = '%s' and item_wall = 1 $item_normal $sql_extra", dbesc($url) ); + if ($j) { xchan_query($j, true); $items = fetch_post_tags($j); } + if ($items) { return self::encode_item(array_shift($items), true); } + return null; } @@ -537,7 +540,7 @@ class Activity { } } - if (intval($i['item_wall']) && $i['mid'] === $i['parent_mid']) { + if (intval($i['item_wall'])) { $ret['commentPolicy'] = map_scope(PermissionLimits::Get($i['uid'], 'post_comments')); } @@ -1586,8 +1589,7 @@ class Activity { } public static function drop($channel, $observer, $act) { - $r = q( - "select * from item where mid = '%s' and uid = %d limit 1", + $r = q("select * from item where mid = '%s' and uid = %d limit 1", dbesc((is_array($act->obj)) ? $act->obj['id'] : $act->obj), intval($channel['channel_id']) ); @@ -1607,7 +1609,6 @@ class Activity { if ($r[0]['item_wall']) { Master::Summon(['Notifier', 'drop', $r[0]['id']]); } - } @@ -1628,11 +1629,15 @@ class Activity { } */ - $url = null; - $ap_hubloc = null; + $url = $person_obj['id'] ?? ''; + + if (!$url) { + return; + } $hublocs = self::get_actor_hublocs($url); $has_zot_hubloc = false; + $ap_hubloc = null; if ($hublocs) { foreach ($hublocs as $hub) { @@ -1656,14 +1661,6 @@ class Activity { } } - if (isset($person_obj['id'])) { - $url = $person_obj['id']; - } - - if (!$url) { - return; - } - $inbox = $person_obj['inbox'] ?? null; // invalid AP identity @@ -2435,6 +2432,10 @@ class Activity { } } + if ($act->type === 'Announce') { + $content['content'] = sprintf(t('🔁 Repeated %1$s\'s %2$s'), $mention, $act->obj['type']); + } + if ($act->type === 'emojiReaction') { $content['content'] = (($act->tgt && $act->tgt['type'] === 'Image') ? '[img=32x32]' . $act->tgt['url'] . '[/img]' : '&#x' . $act->tgt['name'] . ';'); } @@ -3038,7 +3039,7 @@ class Activity { // The $item['item_fetched'] flag is set in fetch_and_store_parents(). // In this case we should check against author permissions because sender is not owner. - if (perm_is_allowed($channel['channel_id'], ((!empty($item['item_fetched'])) ? $item['author_xchan'] : $observer_hash), 'send_stream') || $is_sys_channel) { + if (perm_is_allowed($channel['channel_id'], ((empty($item['item_fetched'])) ? $observer_hash : $item['author_xchan']), 'send_stream') || $is_sys_channel) { $allowed = true; } @@ -3166,6 +3167,10 @@ class Activity { $fetch = false; if (perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') || $is_sys_channel) { + if ($item['verb'] === 'Announce') { + $force = true; + } + $fetch = (($fetch_parents) ? self::fetch_and_store_parents($channel, $observer_hash, $item, $force) : false); } @@ -3182,6 +3187,7 @@ class Activity { return; } + $item['owner_xchan'] = (($item['verb'] === 'Announce') ? $parent[0]['author_xchan'] : $parent[0]['owner_xchan']); if ($parent[0]['parent_mid'] !== $item['parent_mid']) { $item['thr_parent'] = $item['parent_mid']; @@ -3240,12 +3246,6 @@ class Activity { intval($item['uid']) ); if ($r) { - - // If we already have the item, dismiss its announce - if ($act->type === 'Announce') { - return; - } - if ($item['edited'] > $r[0]['edited']) { $item['id'] = $r[0]['id']; $x = item_store_update($item); @@ -3311,6 +3311,8 @@ class Activity { $p = []; + $announce_init = $item['verb'] === 'Announce'; + $current_item = $item; while ($current_item['parent_mid'] !== $current_item['mid']) { @@ -3352,6 +3354,7 @@ class Activity { $item = $hookinfo['item']; if ($item) { + $item['item_fetched'] = true; if (intval($channel['channel_system']) && intval($item['item_private'])) { @@ -3364,9 +3367,19 @@ class Activity { break; } + if ($announce_init) { + // If the fetch was initiated by an announce activity + // do not set item fetched. This way the owner will be set to the + // observer -> the announce actor + unset($item['item_fetched']); + $item['verb'] = 'Announce'; + $item['parent_mid'] = $item['mid']; + $item['item_thread_top'] = 1; + } + array_unshift($p, [$a, $item]); - if ($item['parent_mid'] === $item['mid']) { + if ($announce_init || $item['parent_mid'] === $item['mid']) { break; } } @@ -3374,6 +3387,7 @@ class Activity { $current_item = $item; } + if ($p) { foreach ($p as $pv) { if ($pv[0]->is_valid()) { diff --git a/Zotlabs/Lib/ActivityStreams.php b/Zotlabs/Lib/ActivityStreams.php index f0fb7c9ae..0770f2040 100644 --- a/Zotlabs/Lib/ActivityStreams.php +++ b/Zotlabs/Lib/ActivityStreams.php @@ -345,7 +345,7 @@ class ActivityStreams { if (!$s) { return false; } - return (in_array($s, ['Like', 'Dislike', 'Flag', 'Block', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject', 'emojiReaction', 'EmojiReaction', 'EmojiReact'])); + return (in_array($s, ['Announce', 'Like', 'Dislike', 'Flag', 'Block', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject', 'emojiReaction', 'EmojiReaction', 'EmojiReact'])); } /** diff --git a/Zotlabs/Lib/Enotify.php b/Zotlabs/Lib/Enotify.php index c3f96e103..d8e6f575a 100644 --- a/Zotlabs/Lib/Enotify.php +++ b/Zotlabs/Lib/Enotify.php @@ -149,7 +149,7 @@ class Enotify { if(array_key_exists('item',$params)) { - if(in_array($params['item']['verb'], [ACTIVITY_LIKE, ACTIVITY_DISLIKE])) { + if(in_array($params['item']['verb'], [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_SHARE])) { if(! $always_show_in_notices || !($vnotify & VNOTIFY_LIKE)) { logger('notification: not a visible activity. Ignoring.'); @@ -163,6 +163,9 @@ class Enotify { if(activity_match($params['verb'], ACTIVITY_DISLIKE)) $action = (($moderated) ? t('requested to dislike') : t('disliked')); + if(activity_match($params['verb'], ACTIVITY_SHARE)) + $action = t('repeated'); + } if($params['item']['obj_type'] === 'Answer') @@ -835,18 +838,6 @@ class Enotify { : (($item['obj_type'] === 'Answer') ? sprintf( t('voted on %s\'s poll'), '[bdi]' . $item['owner']['xchan_name'] . '[/bdi]') : sprintf( t('commented on %s\'s post'), '[bdi]' . $item['owner']['xchan_name'] . '[/bdi]')) ); - if($item['verb'] === ACTIVITY_SHARE && empty($item['owner']['xchan_pubforum'])) { - $itemem_text = sprintf( t('repeated %s\'s post'), '[bdi]' . $item['author']['xchan_name'] . '[/bdi]'); - } - - if($item['verb'] === ACTIVITY_LIKE) { - $itemem_text = sprintf( t('liked %s\'s post'), '[bdi]' . $item['author']['xchan_name'] . '[/bdi]'); - } - - if($item['verb'] === ACTIVITY_DISLIKE) { - $itemem_text = sprintf( t('disliked %s\'s post'), '[bdi]' . $item['author']['xchan_name'] . '[/bdi]'); - } - if(in_array($item['obj_type'], ['Document', 'Video', 'Audio', 'Image'])) { $itemem_text = t('shared a file with you'); } @@ -867,7 +858,6 @@ class Enotify { // convert this logic into a json array just like the system notifications - $who = (($item['verb'] === ACTIVITY_SHARE && empty($item['owner']['xchan_pubforum'])) ? 'owner' : 'author'); $body = html2plain(bbcode($item['body'], ['drop_media' => true, 'tryoembed' => false]), 75, true); if ($body) { $body = htmlentities($body, ENT_QUOTES, 'UTF-8', false); @@ -875,10 +865,10 @@ class Enotify { $x = array( 'notify_link' => $item['llink'], - 'name' => $item[$who]['xchan_name'], - 'addr' => $item[$who]['xchan_addr'] ? $item[$who]['xchan_addr'] : $item[$who]['xchan_url'], - 'url' => $item[$who]['xchan_url'], - 'photo' => $item[$who]['xchan_photo_s'], + 'name' => $item['author']['xchan_name'], + 'addr' => $item['author']['xchan_addr'] ? $item['author']['xchan_addr'] : $item['author']['xchan_url'], + 'url' => $item['author']['xchan_url'], + 'photo' => $item['author']['xchan_photo_s'], 'when' => (($edit) ? datetime_convert('UTC', date_default_timezone_get(), $item['edited']) : datetime_convert('UTC', date_default_timezone_get(), $item['created'])), 'class' => (intval($item['item_unseen']) ? 'notify-unseen' : 'notify-seen'), 'b64mid' => (($item['mid']) ? gen_link_id($item['mid']) : ''), @@ -887,7 +877,7 @@ class Enotify { 'message' => bbcode(escape_tags($itemem_text)), 'body' => $body, // these are for the superblock addon - 'hash' => $item[$who]['xchan_hash'], + 'hash' => $item['author']['xchan_hash'], 'uid' => $item['uid'], 'display' => true ); diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php index 0d11704f1..be1ca15c0 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -1204,7 +1204,6 @@ class Libzot { // @fixme; $deliveries = self::public_recips($env, $AS); - } $deliveries = array_unique($deliveries); @@ -1225,10 +1224,6 @@ class Libzot { $author_url = $AS->actor['id']; - if ($AS->type === 'Announce') { - $author_url = Activity::get_attributed_to_actor_url($AS); - } - $r = Activity::get_actor_hublocs($author_url); if (!$r) { @@ -1650,9 +1645,12 @@ class Libzot { } } - } elseif ($permit_mentions) { + } + elseif ($permit_mentions) { $allowed = true; } + + } if ($request) { @@ -1731,11 +1729,15 @@ class Libzot { // the top level post is unlikely to be imported and // this is just an exercise in futility. - if (perm_is_allowed($channel['channel_id'], $sender, 'send_stream')) { - Master::Summon(['Zotconvo', $channel['channel_id'], $arr['parent_mid']]); + if ($arr['verb'] === 'Announce') { + Activity::fetch_and_store_parents($channel, $sender, $arr, true); + } + else { + if (perm_is_allowed($channel['channel_id'], $sender, 'send_stream')) { + Master::Summon(['Zotconvo', $channel['channel_id'], $arr['parent_mid']]); + } + continue; } - - continue; } if ($r[0]['obj_type'] === 'Question') { @@ -1758,6 +1760,8 @@ class Libzot { // so just set the owner and route accordingly. $arr['route'] = $r[0]['route']; $arr['owner_xchan'] = $r[0]['owner_xchan']; + + } else { @@ -1841,13 +1845,6 @@ class Libzot { ); if ($r) { - // We already have this post. - // Dismiss its announce - if ($act->type === 'Announce') { - $DR->update('update ignored'); - $result[] = $DR->get(); - continue; - } $item_id = $r[0]['id']; diff --git a/Zotlabs/Lib/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php index 3cdb59e5f..e7d6e33f8 100644 --- a/Zotlabs/Lib/ThreadItem.php +++ b/Zotlabs/Lib/ThreadItem.php @@ -196,9 +196,14 @@ class ThreadItem { $attend = null; // process action responses - e.g. like/dislike/attend/agree/whatever - $response_verbs = array('like'); - if(feature_enabled($conv->get_profile_owner(),'dislike')) + $response_verbs[] = 'like'; + + if(feature_enabled($conv->get_profile_owner(),'dislike')) { $response_verbs[] = 'dislike'; + } + + $response_verbs[] = 'announce'; + if(in_array($item['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) { $response_verbs[] = 'attendyes'; $response_verbs[] = 'attendno'; @@ -224,6 +229,8 @@ class ThreadItem { $my_responses[$v] = ((isset($conv_responses[$v][$item['mid'] . '-m'])) ? 1 : 0); } +/* + $like_count = ((x($conv_responses['like'],$item['mid'])) ? $conv_responses['like'][$item['mid']] : ''); $like_list = ((x($conv_responses['like'],$item['mid'])) ? $conv_responses['like'][$item['mid'] . '-l'] : ''); if (($like_list) && (count($like_list) > MAX_LIKERS)) { @@ -234,6 +241,16 @@ class ThreadItem { } $like_button_label = tt('Like','Likes',$like_count,'noun'); + $repeat_count = ((x($conv_responses['announce'],$item['mid'])) ? $conv_responses['announce'][$item['mid']] : ''); + $repeat_list = ((x($conv_responses['announce'],$item['mid'])) ? $conv_responses['announce'][$item['mid'] . '-l'] : ''); + if (($repeat_list) && (count($repeat_list) > MAX_LIKERS)) { + $repeat_list_part = array_slice($repeat_list, 0, MAX_LIKERS); + array_push($repeat_list_part, '<a class="dropdown-item" href="#" data-toggle="modal" data-target="#repeatModal-' . $this->get_id() . '"><b>' . t('View all') . '</b></a>'); + } else { + $repeat_list_part = ''; + } + $repeat_button_label = tt('Repeat','Repeats',$repeat_count,'noun'); + $showdislike = ''; if (feature_enabled($conv->get_profile_owner(),'dislike')) { $dislike_count = ((x($conv_responses['dislike'],$item['mid'])) ? $conv_responses['dislike'][$item['mid']] : ''); @@ -250,6 +267,7 @@ class ThreadItem { } $showlike = ((x($conv_responses['like'],$item['mid'])) ? format_like($conv_responses['like'][$item['mid']],$conv_responses['like'][$item['mid'] . '-l'],'like',$item['mid']) : ''); +*/ /* * We should avoid doing this all the time, but it depends on the conversation mode @@ -483,19 +501,29 @@ class ThreadItem { 'markseen' => t('Mark all seen'), 'responses' => $responses, 'my_responses' => $my_responses, + /* 'like_count' => $like_count, 'like_list' => $like_list, 'like_list_part' => $like_list_part, 'like_button_label' => $like_button_label, 'like_modal_title' => t('Likes','noun'), + + 'repeat_count' => $repeat_count, + 'repeat_list' => $repeat_list, + 'repeat_list_part' => $repeat_list_part, + 'repeat_button_label' => $repeat_button_label, + 'repeat_modal_title' => t('Repeats','noun'), + + 'dislike_modal_title' => t('Dislikes','noun'), 'dislike_count' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike_count : ''), 'dislike_list' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike_list : ''), 'dislike_list_part' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike_list_part : ''), 'dislike_button_label' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike_button_label : ''), +*/ 'modal_dismiss' => t('Close'), - 'showlike' => $showlike, - 'showdislike' => $showdislike, + // 'showlike' => $showlike, + // 'showdislike' => $showdislike, 'comment' => ($item['item_delayed'] ? '' : $this->get_comment_box()), 'previewing' => ($conv->is_preview() ? true : false ), 'preview_lbl' => t('This is an unsaved preview'), @@ -507,7 +535,8 @@ class ThreadItem { 'moderate' => ($item['item_blocked'] == ITEM_MODERATED), 'moderate_approve' => t('Approve'), 'moderate_delete' => t('Delete'), - 'rtl' => in_array($item['lang'], rtl_languages()) + 'rtl' => in_array($item['lang'], rtl_languages()), + ); diff --git a/Zotlabs/Module/Like.php b/Zotlabs/Module/Like.php index 54daf6471..4dd43b682 100644 --- a/Zotlabs/Module/Like.php +++ b/Zotlabs/Module/Like.php @@ -21,6 +21,7 @@ class Like extends Controller { $acts = [ 'like' => ACTIVITY_LIKE, 'dislike' => ACTIVITY_DISLIKE, + 'announce' => ACTIVITY_SHARE, 'agree' => ACTIVITY_AGREE, 'disagree' => ACTIVITY_DISAGREE, 'abstain' => ACTIVITY_ABSTAIN, @@ -71,11 +72,12 @@ class Like extends Controller { $activities = q("SELECT item.*, item.id AS item_id FROM item WHERE uid = %d $item_normal AND thr_parent = '%s' - AND verb IN ('%s', '%s', '%s', '%s', '%s')", + AND verb IN ('%s', '%s', '%s', '%s', '%s', '%s')", intval($arr['item']['uid']), dbesc($arr['item']['mid']), dbesc(ACTIVITY_LIKE), dbesc(ACTIVITY_DISLIKE), + dbesc(ACTIVITY_SHARE), dbesc(ACTIVITY_ATTEND), dbesc(ACTIVITY_ATTENDNO), dbesc(ACTIVITY_ATTENDMAYBE) diff --git a/Zotlabs/Module/Share.php b/Zotlabs/Module/Share.php index eb22b5802..248126f7f 100644 --- a/Zotlabs/Module/Share.php +++ b/Zotlabs/Module/Share.php @@ -123,7 +123,7 @@ class Share extends \Zotlabs\Web\Controller { call_hooks('post_local_end', $arr); - info( t('Post repeated') . EOL); + // info( t('Post repeated') . EOL); $r = q("select * from item where id = %d", intval($post_id) @@ -62,7 +62,7 @@ require_once('include/conversation.php'); require_once('include/acl_selectors.php'); define('PLATFORM_NAME', 'hubzilla'); -define('STD_VERSION', '8.9.2'); +define('STD_VERSION', '8.9.3'); define('ZOT_REVISION', '6.0'); define('DB_UPDATE_VERSION', 1261); diff --git a/include/conversation.php b/include/conversation.php index f8d5f7ec0..c7057b09d 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -90,7 +90,7 @@ function item_redir_and_replace_images($body, $images, $cid) { function localize_item(&$item){ - if (activity_match($item['verb'],ACTIVITY_LIKE) || activity_match($item['verb'],ACTIVITY_DISLIKE)){ + if (activity_match($item['verb'],ACTIVITY_LIKE) || activity_match($item['verb'],ACTIVITY_DISLIKE) || activity_match($item['verb'],ACTIVITY_SHARE)){ if(! $item['obj']) return; @@ -195,6 +195,10 @@ function localize_item(&$item){ elseif(activity_match($item['verb'],ACTIVITY_DISLIKE)) { $bodyverb = t('%1$s doesn\'t like %2$s\'s %3$s'); } + elseif(activity_match($item['verb'],ACTIVITY_SHARE)) { + $bodyverb = t('%1$s repeated %2$s\'s %3$s'); + } + // short version, in notification strings the author will be displayed separately @@ -204,6 +208,9 @@ function localize_item(&$item){ elseif(activity_match($item['verb'],ACTIVITY_DISLIKE)) { $shortbodyverb = t('doesn\'t like %1$s\'s %2$s'); } + elseif(activity_match($item['verb'],ACTIVITY_SHARE)) { + $shortbodyverb = t('repeated %1$s\'s %2$s'); + } $item['shortlocalize'] = sprintf($shortbodyverb, '[bdi]' . $author_name . '[/bdi]', $post_type); @@ -441,7 +448,7 @@ function count_descendants($item) { * @return boolean */ function visible_activity($item) { - $hidden_activities = [ ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_AGREE, ACTIVITY_DISAGREE, ACTIVITY_ABSTAIN, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE, ACTIVITY_POLLRESPONSE ]; + $hidden_activities = [ ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_SHARE, ACTIVITY_AGREE, ACTIVITY_DISAGREE, ACTIVITY_ABSTAIN, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE, ACTIVITY_POLLRESPONSE ]; if(intval($item['item_notshown'])) return false; @@ -672,7 +679,8 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa 'attendyes' => ['title' => t('Attending','title')], 'attendno' => ['title' => t('Not attending','title')], 'attendmaybe' => ['title' => t('Might attend','title')], - 'answer' => [] + 'answer' => [], + 'announce' => ['title' => t('Repeats','title')], ]; @@ -910,8 +918,6 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa continue; } - - $item['pagedrop'] = $page_dropping; if($item['id'] == $item['parent'] || $r_preview) { @@ -1217,6 +1223,9 @@ function builtin_activity_puller($item, &$conv_responses) { case 'answer': $verb = ACTIVITY_POST; break; + case 'announce': + $verb = 'Announce'; + break; default: return; break; @@ -1774,28 +1783,31 @@ function get_responses($conv_responses,$response_verbs,$ob,$item) { function get_response_button_text($v,$count) { switch($v) { case 'like': - return tt('Like','Likes',$count,'noun'); + return ['label' => tt('Like','Likes',$count,'noun'), 'icon' => 'thumbs-o-up', 'class' => 'like']; + break; + case 'announce': + return ['label' => tt('Repeat','Repeats',$count,'noun'), 'icon' => 'retweet', 'class' => 'announce']; break; case 'dislike': - return tt('Dislike','Dislikes',$count,'noun'); + return ['label' => tt('Dislike','Dislikes',$count,'noun'), 'icon' => 'thumbs-o-down', 'class' => 'dislike']; break; case 'attendyes': - return tt('Attending','Attending',$count,'noun'); + return ['label' => tt('Attending','Attending',$count,'noun'), 'icon' => 'calendar-check-o', 'class' => 'attendyes']; break; case 'attendno': - return tt('Not Attending','Not Attending',$count,'noun'); + return ['label' => tt('Not Attending','Not Attending',$count,'noun'), 'icon' => 'calendar-times-o', 'class' => 'attendno']; break; case 'attendmaybe': - return tt('Undecided','Undecided',$count,'noun'); + return ['label' => tt('Undecided','Undecided',$count,'noun'), 'icon' => 'calendar-o', 'class' => 'attendmaybe']; break; case 'agree': - return tt('Agree','Agrees',$count,'noun'); + return ['label' => tt('Agree','Agrees',$count,'noun'), 'icon' => '', 'class' => '']; break; case 'disagree': - return tt('Disagree','Disagrees',$count,'noun'); + return ['label' => tt('Disagree','Disagrees',$count,'noun'), 'icon' => '', 'class' => '']; break; case 'abstain': - return tt('Abstain','Abstains',$count,'noun'); + return ['label' => tt('Abstain','Abstains',$count,'noun'), 'icon' => '', 'class' => '']; break; default: return ''; diff --git a/include/network.php b/include/network.php index f5c5303b3..c5411e702 100644 --- a/include/network.php +++ b/include/network.php @@ -591,23 +591,30 @@ function validate_url(&$url) { } /** - * @brief Checks that email is an actual resolvable internet address. + * @brief Checks that email is valid, and that the domain resolves. * - * @param string $addr - * @return boolean + * Note: This does not try to check that the actual email address will resolve, + * only the domain! + * + * @param string $addr The email address to validate. + * @return boolean True if email is valid, false otherwise. */ -function validate_email($addr) { +function validate_email(string $addr): bool { if(get_config('system', 'disable_email_validation')) return true; - if(! strpos($addr, '@')) - return false; - - $h = substr($addr, strpos($addr, '@') + 1); + $matches = array(); + $result = preg_match( + '/^[A-Z0-9._%-]+@([A-Z0-9.-]+\.[A-Z0-9-]{2,})$/i', + punify($addr), + $matches); - if(($h) && z_dns_check($h, true)) { - return true; + if($result) { + $domain = $matches[1]; + if(($domain) && z_dns_check($domain, true)) { + return true; + } } return false; diff --git a/tests/unit/UnitTestCase.php b/tests/unit/UnitTestCase.php index 97ac1576e..0bf7b547a 100644 --- a/tests/unit/UnitTestCase.php +++ b/tests/unit/UnitTestCase.php @@ -72,6 +72,8 @@ 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. diff --git a/tests/unit/includes/NetworkTest.php b/tests/unit/includes/NetworkTest.php index 0b9b42e00..9fb00e9d3 100644 --- a/tests/unit/includes/NetworkTest.php +++ b/tests/unit/includes/NetworkTest.php @@ -5,29 +5,68 @@ * @package test.util */ -use PHPUnit\Framework\TestCase; - -require_once('include/network.php'); - -class NetworkTest extends TestCase { - - public function setup() : void { - \App::set_baseurl("https://mytest.org"); - } - - /** - * @dataProvider localUrlTestProvider - */ - public function testIsLocalURL($url, $expected) { - $this->assertEquals($expected, is_local_url($url)); - } - - public function localUrlTestProvider() : array { - return [ - [ '/some/path', true ], - [ 'https://mytest.org/some/path', true ], - [ 'https://other.site/some/path', false ], - ]; - } -} +class NetworkTest extends Zotlabs\Tests\Unit\UnitTestCase { + + public function setUp() : void { + parent::setUp(); + + \App::set_baseurl("https://mytest.org"); + } + + /** + * @dataProvider localUrlTestProvider + */ + public function testIsLocalURL($url, $expected) { + $this->assertEquals($expected, is_local_url($url)); + } + + public function localUrlTestProvider() : array { + return [ + [ '/some/path', true ], + [ 'https://mytest.org/some/path', true ], + [ 'https://other.site/some/path', false ], + ]; + } + + /** + * Test the validate_email function. + * + * @dataProvider validate_email_provider + */ + public function test_validate_email(string $email, bool $expected) : void { + $this->assertEquals($expected, validate_email($email)); + } + /** + * Test that the validate_email function is disabled when configured to. + * + * @dataProvider validate_email_provider + */ + public function test_disable_validate_email(string $email) : void { + \Zotlabs\Lib\Config::Set('system', 'disable_email_validation', true); + $this->assertTrue(validate_email($email)); + } + + function validate_email_provider() : array { + return [ + // First some invalid email addresses + ['', false], + ['not_an_email', false], + ['@not_an_email', false], + ['not@an@email', false], + ['not@an@email.com', false], + + // then test valid addresses too + ['test@example.com', true], + + // Should also work with international domains + ['some.email@dømain.net', true], + + // Should also work with the new top-level domains + ['some.email@example.cancerresearch', true], + + // And internationalized TLD's + ['some.email@example.شبكة', true] + ]; + } +} diff --git a/tests/unit/includes/dba/_files/config.yml b/tests/unit/includes/dba/_files/config.yml index 80b9ba12f..e93486857 100644 --- a/tests/unit/includes/dba/_files/config.yml +++ b/tests/unit/includes/dba/_files/config.yml @@ -1,13 +1,23 @@ --- config: - - id: 1 cat: system k: do_not_check_dns v: true - - id: 2 cat: system k: not_allowed_email v: 'baduser@example.com,baddomain.com,*.evil.org' + - + cat: system + k: debugging + v: true + - + cat: system + k: loglevel + v: 2 + - + cat: system + k: logfile + v: tests/results/unit_test.log diff --git a/view/js/main.js b/view/js/main.js index d488a402e..127b41197 100644 --- a/view/js/main.js +++ b/view/js/main.js @@ -1358,7 +1358,7 @@ function dostar(ident) { $('#starred-' + ident).removeClass('fa-star-o'); $('#star-' + ident).addClass('hidden'); $('#unstar-' + ident).removeClass('hidden'); - var btn_tpl = '<div class="btn-group" id="star-button-' + ident + '"><button type="button" class="btn btn-outline-secondary btn-sm wall-item-like" onclick="dostar(' + ident + ');"><i class="fa fa-star"></i></button></div>' + var btn_tpl = '<div class="btn-group" id="star-button-' + ident + '"><button type="button" class="btn btn-outline-secondary border-0 btn-sm wall-item-star" onclick="dostar(' + ident + ');"><i class="fa fa-star"></i></button></div>' $('#wall-item-tools-left-' + ident).prepend(btn_tpl); } else { diff --git a/view/tpl/conv_item.tpl b/view/tpl/conv_item.tpl index 930137be8..4194bc4da 100644 --- a/view/tpl/conv_item.tpl +++ b/view/tpl/conv_item.tpl @@ -238,22 +238,22 @@ </div> {{if $item.responses || $item.attachments}} - <div class="wall-item-tools-left btn-group" id="wall-item-tools-left-{{$item.id}}"> + <div class="wall-item-tools-left hstack gap-1" id="wall-item-tools-left-{{$item.id}}"> {{if $item.star && $item.star.isstarred}} - <div class="btn-group" id="star-button-{{$item.id}}"> - <button type="button" class="btn btn-outline-secondary btn-sm wall-item-like" onclick="dostar({{$item.id}});"><i class="fa fa-star"></i></button> + <div class="" id="star-button-{{$item.id}}"> + <button type="button" class="btn btn-outline-secondary border-0 btn-sm wall-item-star" onclick="dostar({{$item.id}});"><i class="fa fa-star"></i></button> </div> {{/if}} {{if $item.attachments}} - <div class="btn-group"> - <button type="button" class="btn btn-outline-secondary btn-sm wall-item-like dropdown-toggle" data-bs-toggle="dropdown" id="attachment-menu-{{$item.id}}"><i class="fa fa-paperclip"></i></button> + <div class=""> + <button type="button" class="btn btn-outline-secondary border-0 btn-sm wall-item-attach" data-bs-toggle="dropdown" id="attachment-menu-{{$item.id}}"><i class="fa fa-paperclip"></i></button> <div class="dropdown-menu">{{$item.attachments}}</div> </div> {{/if}} {{foreach $item.responses as $verb=>$response}} {{if $response.count}} - <div class="btn-group"> - <button type="button" class="btn btn-outline-secondary btn-sm wall-item-like dropdown-toggle"{{if $response.modal}} data-bs-toggle="modal" data-bs-target="#{{$verb}}Modal-{{$item.id}}"{{else}} data-bs-toggle="dropdown"{{/if}} id="wall-item-{{$verb}}-{{$item.id}}">{{$response.count}} {{$response.button}}</button> + <div class=""> + <button type="button" title="{{$response.count}} {{$response.button.label}}" class="btn btn-outline-secondary border-0 btn-sm wall-item-{{$response.button.class}}"{{if $response.modal}} data-bs-toggle="modal" data-bs-target="#{{$verb}}Modal-{{$item.id}}"{{else}} data-bs-toggle="dropdown"{{/if}} id="wall-item-{{$verb}}-{{$item.id}}"><i class="fa fa-{{$response.button.icon}}"></i> {{$response.count}}</button> {{if $response.modal}} <div class="modal" id="{{$verb}}Modal-{{$item.id}}"> <div class="modal-dialog"> |