aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml17
-rw-r--r--CHANGELOG4
-rw-r--r--Zotlabs/Lib/Activity.php58
-rw-r--r--Zotlabs/Lib/ActivityStreams.php2
-rw-r--r--Zotlabs/Lib/Enotify.php28
-rw-r--r--Zotlabs/Lib/Libzot.php31
-rw-r--r--Zotlabs/Lib/ThreadItem.php39
-rw-r--r--Zotlabs/Module/Like.php4
-rw-r--r--Zotlabs/Module/Share.php2
-rw-r--r--boot.php2
-rw-r--r--include/conversation.php38
-rw-r--r--include/network.php27
-rw-r--r--tests/unit/UnitTestCase.php2
-rw-r--r--tests/unit/includes/NetworkTest.php89
-rw-r--r--tests/unit/includes/dba/_files/config.yml14
-rw-r--r--view/js/main.js2
-rw-r--r--view/tpl/conv_item.tpl14
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
diff --git a/CHANGELOG b/CHANGELOG
index e35223071..ca7bb5f07 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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('&#x1f501; 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)
diff --git a/boot.php b/boot.php
index 30672e44b..2ccdd12d0 100644
--- a/boot.php
+++ b/boot.php
@@ -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">