diff --git a/.travis.yml b/.travis.yml index c3d009201..4ea3ac7ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ services: - redis-server - memcached env: - - MYSQL_HOST=localhost MYSQL_PORT=3306 MYSQL_USERNAME=travis MYSQL_PASSWORD= MYSQL_DATABASE=test + - MYSQL_HOST=localhost MYSQL_PORT=3306 MYSQL_USERNAME=travis MYSQL_PASSWORD="" MYSQL_DATABASE=test install: - composer install diff --git a/database.sql b/database.sql index 9d4086994..04a35634e 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2020.03-dev (Dalmatian Bellflower) --- DB_UPDATE_VERSION 1333 +-- DB_UPDATE_VERSION 1334 -- ------------------------------------------ @@ -179,7 +179,7 @@ CREATE TABLE IF NOT EXISTS `contact` ( `location` varchar(255) DEFAULT '' COMMENT '', `about` text COMMENT '', `keywords` text COMMENT 'public keywords (interests) of the contact', - `gender` varchar(32) NOT NULL DEFAULT '' COMMENT '', + `gender` varchar(32) NOT NULL DEFAULT '' COMMENT 'Deprecated', `xmpp` varchar(255) NOT NULL DEFAULT '' COMMENT '', `attag` varchar(255) NOT NULL DEFAULT '' COMMENT '', `avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '', @@ -397,7 +397,7 @@ CREATE TABLE IF NOT EXISTS `gcontact` ( `location` varchar(255) NOT NULL DEFAULT '' COMMENT '', `about` text COMMENT '', `keywords` text COMMENT 'puplic keywords (interests)', - `gender` varchar(32) NOT NULL DEFAULT '' COMMENT '', + `gender` varchar(32) NOT NULL DEFAULT '' COMMENT 'Deprecated', `birthday` varchar(32) NOT NULL DEFAULT '0001-01-01' COMMENT '', `community` boolean NOT NULL DEFAULT '0' COMMENT '1 if contact is forum account', `contact-type` tinyint NOT NULL DEFAULT -1 COMMENT '', @@ -568,7 +568,7 @@ CREATE TABLE IF NOT EXISTS `item` ( `extid` varchar(255) NOT NULL DEFAULT '' COMMENT '', `post-type` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Post type (personal note, bookmark, ...)', `global` boolean NOT NULL DEFAULT '0' COMMENT '', - `private` boolean NOT NULL DEFAULT '0' COMMENT 'distribution is restricted', + `private` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0=public, 1=private, 2=unlisted', `visible` boolean NOT NULL DEFAULT '0' COMMENT '', `moderated` boolean NOT NULL DEFAULT '0' COMMENT '', `deleted` boolean NOT NULL DEFAULT '0' COMMENT 'item has been deleted', @@ -1057,19 +1057,18 @@ CREATE TABLE IF NOT EXISTS `profile_check` ( -- TABLE profile_field -- CREATE TABLE IF NOT EXISTS `profile_field` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'sequential ID', - `uid` mediumint(8) unsigned NOT NULL DEFAULT 0 COMMENT 'Owner user id', - `psid` int(10) unsigned DEFAULT NULL COMMENT 'ID of the permission set of this profile field - 0 = public', - `name` varchar(255) NOT NULL DEFAULT '' COMMENT 'Name of the field', - `value` text COMMENT 'Value of the field', - `order` mediumint(8) unsigned NOT NULL DEFAULT 1 COMMENT 'Field ordering per user', - `label` varchar(255) NOT NULL DEFAULT '' COMMENT 'Label of the field', - `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', - `edited` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', - PRIMARY KEY (`id`), - KEY `uid` (`uid`), - KEY `psid` (`psid`), - KEY `order` (`order`) + `id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID', + `uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'Owner user id', + `order` mediumint unsigned NOT NULL DEFAULT 1 COMMENT 'Field ordering per user', + `psid` int unsigned COMMENT 'ID of the permission set of this profile field - 0 = public', + `label` varchar(255) NOT NULL DEFAULT '' COMMENT 'Label of the field', + `value` text COMMENT 'Value of the field', + `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'creation time', + `edited` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'last edit time', + PRIMARY KEY(`id`), + INDEX `uid` (`uid`), + INDEX `order` (`order`), + INDEX `psid` (`psid`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Custom profile fields'; -- @@ -1179,7 +1178,7 @@ CREATE TABLE IF NOT EXISTS `thread` ( `received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', `changed` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', `wall` boolean NOT NULL DEFAULT '0' COMMENT '', - `private` boolean NOT NULL DEFAULT '0' COMMENT '', + `private` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0=public, 1=private, 2=unlisted', `pubmail` boolean NOT NULL DEFAULT '0' COMMENT '', `moderated` boolean NOT NULL DEFAULT '0' COMMENT '', `visible` boolean NOT NULL DEFAULT '0' COMMENT '', diff --git a/doc/BBCode.md b/doc/BBCode.md index b13d08119..cab51bd09 100644 --- a/doc/BBCode.md +++ b/doc/BBCode.md @@ -113,17 +113,17 @@ table.bbcodes > * > tr > th {
' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8'); - $headline .= DI::l10n()->t('%2$s %3$s', $attributes['link'], $mention, $attributes['posted']); + $headline .= DI::l10n()->t('%2$s %3$s', $attributes['link'], $mention, $attributes['posted']); $headline .= ':
' . "\n"; $text = ($is_quote_share? '' . trim($content) . '' . "\n"; @@ -1636,9 +1637,9 @@ class BBCode $text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", $try_oembed_callback, $text); } else { $text = preg_replace("/\[video\](.*?)\[\/video\]/ism", - '$1', $text); + '$1', $text); $text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", - '$1', $text); + '$1', $text); } // html5 video and audio @@ -1665,7 +1666,7 @@ class BBCode $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '', $text); } else { $text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", - 'https://www.youtube.com/watch?v=$1', $text); + 'https://www.youtube.com/watch?v=$1', $text); } if ($try_oembed) { @@ -1680,7 +1681,7 @@ class BBCode $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '', $text); } else { $text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", - 'https://vimeo.com/$1', $text); + 'https://vimeo.com/$1', $text); } // oembed tag @@ -1801,17 +1802,17 @@ class BBCode . ''; }, $text); - // We need no target="_blank" for local links - // convert links start with DI::baseUrl() as local link without the target="_blank" attribute + // We need no target="_blank" rel="noopener noreferrer" for local links + // convert links start with DI::baseUrl() as local link without the target="_blank" rel="noopener noreferrer" attribute $escapedBaseUrl = preg_quote(DI::baseUrl(), '/'); $text = preg_replace("/\[url\](".$escapedBaseUrl.".*?)\[\/url\]/ism", '$1', $text); $text = preg_replace("/\[url\=(".$escapedBaseUrl.".*?)\](.*?)\[\/url\]/ism", '$2', $text); - $text = preg_replace("/\[url\](.*?)\[\/url\]/ism", '$1', $text); - $text = preg_replace("/\[url\=(.*?)\](.*?)\[\/url\]/ism", '$2', $text); + $text = preg_replace("/\[url\](.*?)\[\/url\]/ism", '$1', $text); + $text = preg_replace("/\[url\=(.*?)\](.*?)\[\/url\]/ism", '$2', $text); // Red compatibility, though the link can't be authenticated on Friendica - $text = preg_replace("/\[zrl\=(.*?)\](.*?)\[\/zrl\]/ism", '$2', $text); + $text = preg_replace("/\[zrl\=(.*?)\](.*?)\[\/zrl\]/ism", '$2', $text); // we may need to restrict this further if it picks up too many strays @@ -2004,8 +2005,6 @@ class BBCode */ public static function toMarkdown($text, $for_diaspora = true) { - $a = DI::app(); - $original_text = $text; // Since Diaspora is creating a summary for links, this function removes them before posting diff --git a/src/Content/Text/HTML.php b/src/Content/Text/HTML.php index 089c5d368..593be7d5f 100644 --- a/src/Content/Text/HTML.php +++ b/src/Content/Text/HTML.php @@ -943,7 +943,7 @@ class HTML */ public static function toLink($s) { - $s = preg_replace("/(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\'\%\$\!\+]*)/", ' $1', $s); + $s = preg_replace("/(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\'\%\$\!\+]*)/", ' $1', $s); $s = preg_replace("/\<(.*?)(src|href)=(.*?)\&\;(.*?)\>/ism", '<$1$2=$3&$4>', $s); return $s; } diff --git a/src/Content/Text/Markdown.php b/src/Content/Text/Markdown.php index bcbf5191a..8dfe00190 100644 --- a/src/Content/Text/Markdown.php +++ b/src/Content/Text/Markdown.php @@ -53,6 +53,8 @@ class Markdown return $url; }; + $text = self::convertDiasporaMentionsToHtml($text); + $html = $MarkdownParser->transform($text); DI::profiler()->saveTimestamp($stamp1, "parser", System::callstack()); @@ -61,35 +63,42 @@ class Markdown } /** - * Callback function to replace a Diaspora style mention in a mention for Friendica + * Replace Diaspora-style mentions in a text since they trip the Markdown parser autolinker. * - * @param array $match Matching values for the callback - * [1] = mention type (@ or !) - * [2] = name (optional) - * [3] = address - * @return string Replaced mention - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException + * @param string $text + * @return string */ - private static function diasporaMention2BBCodeCallback($match) + private static function convertDiasporaMentionsToHtml(string $text) { - if ($match[3] == '') { - return; - } + return preg_replace_callback( + '/([@!]){(?:([^}]+?); ?)?([^} ]+)}/', + /* + * Matching values for the callback + * [1] = mention type (@ or !) + * [2] = name (optional) + * [3] = profile URL + */ + function ($matches) { + if ($matches[3] == '') { + return ''; + } - $data = Contact::getDetailsByAddr($match[3]); + $data = Contact::getDetailsByAddr($matches[3]); - if (empty($data)) { - return; - } + if (empty($data)) { + return ''; + } - $name = $match[2]; + $name = $matches[2]; - if ($name == '') { - $name = $data['name']; - } + if ($name == '') { + $name = $data['name']; + } - return $match[1] . '[url=' . $data['url'] . ']' . $name . '[/url]'; + return $matches[1] . '' . $name . ''; + }, + $text + ); } /* @@ -110,9 +119,6 @@ class Markdown $s = self::convert($s); - $regexp = "/([@!])\{(?:([^\}]+?); ?)?([^\} ]+)\}/"; - $s = preg_replace_callback($regexp, ['self', 'diasporaMention2BBCodeCallback'], $s); - $s = HTML::toBBCode($s); // protect the recycle symbol from turning into a tag, but without unescaping angles and naked ampersands diff --git a/src/Core/Console.php b/src/Core/Console.php index 70835db9c..86178c209 100644 --- a/src/Core/Console.php +++ b/src/Core/Console.php @@ -57,7 +57,7 @@ Commands: autoinstall Starts automatic installation of friendica based on values from htconfig.php lock Edit site locks maintenance Set maintenance mode for this node - newpassword Set a new password for a given user + user User management php2po Generate a messages.po file from a strings.php file po2php Generate a strings.php file from a messages.po file typo Checks for parse errors in Friendica files @@ -85,7 +85,7 @@ HELP; 'autoinstall' => Friendica\Console\AutomaticInstallation::class, 'lock' => Friendica\Console\Lock::class, 'maintenance' => Friendica\Console\Maintenance::class, - 'newpassword' => Friendica\Console\NewPassword::class, + 'user' => Friendica\Console\User::class, 'php2po' => Friendica\Console\PhpToPo::class, 'po2php' => Friendica\Console\PoToPhp::class, 'typo' => Friendica\Console\Typo::class, diff --git a/src/Core/L10n.php b/src/Core/L10n.php index cda83ac3f..8e6ee171c 100644 --- a/src/Core/L10n.php +++ b/src/Core/L10n.php @@ -33,6 +33,9 @@ use Psr\Log\LoggerInterface; */ class L10n { + /** @var string The default language */ + const DEFAULT = 'en'; + /** * A string indicating the current language used for translation: * - Two-letter ISO 639-1 code. @@ -64,7 +67,7 @@ class L10n $this->dba = $dba; $this->logger = $logger; - $this->loadTranslationTable(L10n::detectLanguage($server, $get, $config->get('system', 'language', 'en'))); + $this->loadTranslationTable(L10n::detectLanguage($server, $get, $config->get('system', 'language', self::DEFAULT))); $this->setSessionVariable($session); $this->setLangFromSession($session); } @@ -158,7 +161,7 @@ class L10n * * @return string The two-letter language code */ - public static function detectLanguage(array $server, array $get, string $sysLang = 'en') + public static function detectLanguage(array $server, array $get, string $sysLang = self::DEFAULT) { $lang_variable = $server['HTTP_ACCEPT_LANGUAGE'] ?? null; diff --git a/src/Core/Theme.php b/src/Core/Theme.php index c17c67c4d..03f1dfd9c 100644 --- a/src/Core/Theme.php +++ b/src/Core/Theme.php @@ -98,7 +98,7 @@ class Theme $comment_lines = explode("\n", $matches[0]); foreach ($comment_lines as $comment_line) { $comment_line = trim($comment_line, "\t\n\r */"); - if ($comment_line != "") { + if (strpos($comment_line, ':') !== false) { list($key, $value) = array_map("trim", explode(":", $comment_line, 2)); $key = strtolower($key); if ($key == "author") { diff --git a/src/Model/Contact.php b/src/Model/Contact.php index f86d3f378..68bd0986a 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -1037,6 +1037,7 @@ class Contact } if (DBA::isResult($r)) { + $authoritativeResult = true; // If there is more than one entry we filter out the connector networks if (count($r) > 1) { foreach ($r as $id => $result) { @@ -1070,6 +1071,7 @@ class Contact $profile["bd"] = DBA::NULL_DATE; } } else { + $authoritativeResult = false; $profile = $default; } @@ -1106,7 +1108,11 @@ class Contact $profile["birthday"] = DBA::NULL_DATE; } - $cache[$url][$uid] = $profile; + // Only cache the result if it came from the DB since this method is used in widely different contexts + // @see display_fetch_author for an example of $default parameter diverging from the DB result + if ($authoritativeResult) { + $cache[$url][$uid] = $profile; + } return $profile; } diff --git a/src/Model/Item.php b/src/Model/Item.php index b960ba38f..eac3b7028 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -113,6 +113,10 @@ class Item Activity::FOLLOW, Activity::ANNOUNCE]; + const PUBLIC = 0; + const PRIVATE = 1; + const UNLISTED = 2; + private static $legacy_mode = null; public static function isLegacyMode() @@ -1112,6 +1116,7 @@ class Item */ public static function deleteById($item_id, $priority = PRIORITY_HIGH) { + Logger::notice('Delete item by id', ['id' => $item_id, 'callstack' => System::callstack()]); // locate item to be deleted $fields = ['id', 'uri', 'uid', 'parent', 'parent-uri', 'origin', 'deleted', 'file', 'resource-id', 'event-id', 'attach', @@ -1541,7 +1546,7 @@ class Item $item['allow_gid'] = trim($item['allow_gid'] ?? ''); $item['deny_cid'] = trim($item['deny_cid'] ?? ''); $item['deny_gid'] = trim($item['deny_gid'] ?? ''); - $item['private'] = intval($item['private'] ?? 0); + $item['private'] = intval($item['private'] ?? self::PUBLIC); $item['body'] = trim($item['body'] ?? ''); $item['tag'] = trim($item['tag'] ?? ''); $item['attach'] = trim($item['attach'] ?? ''); @@ -1737,8 +1742,8 @@ class Item * The original author commented, but as this is a comment, the permissions * weren't fixed up so it will still show the comment as private unless we fix it here. */ - if ((intval($parent['forum_mode']) == 1) && $parent['private']) { - $item['private'] = 0; + if ((intval($parent['forum_mode']) == 1) && ($parent['private'] != self::PUBLIC)) { + $item['private'] = self::PUBLIC; } // If its a post that originated here then tag the thread as "mention" @@ -1808,7 +1813,7 @@ class Item // ACL settings if (strlen($allow_cid) || strlen($allow_gid) || strlen($deny_cid) || strlen($deny_gid)) { - $private = 1; + $private = self::PRIVATE; } else { $private = $item['private']; } @@ -1934,7 +1939,7 @@ class Item if ($entries > 1) { // There are duplicates. We delete our just created entry. - Logger::log('Duplicated post occurred. uri = ' . $item['uri'] . ' uid = ' . $item['uid']); + Logger::notice('Delete duplicated item', ['id' => $current_post, 'uri' => $item['uri'], 'uid' => $item['uid']]); // Yes, we could do a rollback here - but we are having many users with MyISAM. DBA::delete('item', ['id' => $current_post]); @@ -2217,7 +2222,7 @@ class Item // Only distribute public items from native networks $condition = ['id' => $itemid, 'uid' => 0, 'network' => array_merge(Protocol::FEDERATED ,['']), - 'visible' => true, 'deleted' => false, 'moderated' => false, 'private' => false]; + 'visible' => true, 'deleted' => false, 'moderated' => false, 'private' => [self::PUBLIC, self::UNLISTED]]; $item = self::selectFirst(self::ITEM_FIELDLIST, $condition); if (!DBA::isResult($item)) { return; @@ -2367,7 +2372,7 @@ class Item } // Is it a visible public post? - if (!$item["visible"] || $item["deleted"] || $item["moderated"] || $item["private"]) { + if (!$item["visible"] || $item["deleted"] || $item["moderated"] || ($item["private"] == Item::PRIVATE)) { return; } @@ -2558,7 +2563,7 @@ class Item Contact::unmarkForArchival($contact); } - $update = (!$arr['private'] && ((($arr['author-link'] ?? '') === ($arr['owner-link'] ?? '')) || ($arr["parent-uri"] === $arr["uri"]))); + $update = (($arr['private'] != self::PRIVATE) && ((($arr['author-link'] ?? '') === ($arr['owner-link'] ?? '')) || ($arr["parent-uri"] === $arr["uri"]))); // Is it a forum? Then we don't care about the rules from above if (!$update && in_array($arr["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN]) && ($arr["parent-uri"] === $arr["uri"])) { @@ -2572,7 +2577,7 @@ class Item ['id' => $arr['contact-id']]); } // Now do the same for the system wide contacts with uid=0 - if (!$arr['private']) { + if ($arr['private'] != self::PRIVATE) { DBA::update('contact', ['success_update' => $arr['received'], 'last-item' => $arr['received']], ['id' => $arr['owner-id']]); @@ -2717,9 +2722,7 @@ class Item if (!$mention) { if (($community_page || $prvgroup) && !$item['wall'] && !$item['origin'] && ($item['id'] == $item['parent'])) { - // mmh.. no mention.. community page or private group... no wall.. no origin.. top-post (not a comment) - // delete it! - Logger::log("no-mention top-level post to community or private group. delete."); + Logger::notice('Delete private group/communiy top-level item without mention', ['id' => $item_id]); DBA::delete('item', ['id' => $item_id]); return true; } @@ -2753,7 +2756,7 @@ class Item // also reset all the privacy bits to the forum default permissions - $private = ($user['allow_cid'] || $user['allow_gid'] || $user['deny_cid'] || $user['deny_gid']) ? 1 : 0; + $private = ($user['allow_cid'] || $user['allow_gid'] || $user['deny_cid'] || $user['deny_gid']) ? self::PRIVATE : self::PUBLIC; $psid = PermissionSet::getIdFromACL( $user['uid'], @@ -2800,7 +2803,7 @@ class Item return false; } - if (($contact['network'] != Protocol::FEED) && $datarray['private']) { + if (($contact['network'] != Protocol::FEED) && ($datarray['private'] == self::PRIVATE)) { Logger::log('Not public', Logger::DEBUG); return false; } @@ -2838,7 +2841,7 @@ class Item $urlpart = parse_url($datarray2['author-link']); $datarray["app"] = $urlpart["host"]; } else { - $datarray['private'] = 0; + $datarray['private'] = self::PUBLIC; } } @@ -3367,7 +3370,7 @@ class Item $condition = ["`uri` = ? AND NOT `deleted` AND NOT (`uid` IN (?, 0))", $itemuri, $item["uid"]]; if (!self::exists($condition)) { DBA::delete('item', ['uri' => $itemuri, 'uid' => 0]); - Logger::log("deleteThread: Deleted shadow for item ".$itemuri, Logger::DEBUG); + Logger::debug('Deleted shadow item', ['id' => $itemid, 'uri' => $itemuri]); } } } @@ -3382,7 +3385,7 @@ class Item * * default permissions - anonymous user */ - $sql = " AND NOT `item`.`private`"; + $sql = sprintf(" AND `item`.`private` != %d", self::PRIVATE); // Profile owner - everything is visible if ($local_user && ($local_user == $owner_id)) { @@ -3398,12 +3401,12 @@ class Item $set = PermissionSet::get($owner_id, $remote_user); if (!empty($set)) { - $sql_set = " OR (`item`.`private` IN (1,2) AND `item`.`wall` AND `item`.`psid` IN (" . implode(',', $set) . "))"; + $sql_set = sprintf(" OR (`item`.`private` = %d AND `item`.`wall` AND `item`.`psid` IN (", self::PRIVATE) . implode(',', $set) . "))"; } else { $sql_set = ''; } - $sql = " AND (NOT `item`.`private`" . $sql_set . ")"; + $sql = sprintf(" AND (`item`.`private` != %d", self::PRIVATE) . $sql_set . ")"; } return $sql; @@ -3505,7 +3508,7 @@ class Item continue; } - if ((local_user() == $item['uid']) && ($item['private'] == 1) && ($item['contact-id'] != $app->contact['id']) && ($item['network'] == Protocol::DFRN)) { + if ((local_user() == $item['uid']) && ($item['private'] == self::PRIVATE) && ($item['contact-id'] != $app->contact['id']) && ($item['network'] == Protocol::DFRN)) { $img_url = 'redir/' . $item['contact-id'] . '?url=' . urlencode($mtch[1]); $item['body'] = str_replace($mtch[0], '[img]' . $img_url . '[/img]', $item['body']); } @@ -3630,7 +3633,7 @@ class Item $title .= ' ' . $mtch[2] . ' ' . DI::l10n()->t('bytes'); $icon = ''; - $as .= '' . $icon . ''; + $as .= '' . $icon . ''; } if ($as != '') { @@ -3683,7 +3686,7 @@ class Item $ret["title"] = DI::l10n()->t('link to source'); } - } elseif (!empty($item['plink']) && ($item['private'] != 1)) { + } elseif (!empty($item['plink']) && ($item['private'] != self::PRIVATE)) { $ret = [ 'href' => $item['plink'], 'orig' => $item['plink'], diff --git a/src/Model/Profile.php b/src/Model/Profile.php index cf9e7c620..867a6db4f 100644 --- a/src/Model/Profile.php +++ b/src/Model/Profile.php @@ -330,7 +330,8 @@ class Profile if (!$local_user_is_self && $show_connect) { if (!$visitor_is_authenticated) { - if (!empty($profile['nickname'])) { + // Remote follow is only available for local profiles + if (!empty($profile['nickname']) && strpos($profile_url, DI::baseUrl()->get()) === 0) { $follow_link = 'remote_follow/' . $profile['nickname']; } } elseif ($profile_is_native) { diff --git a/src/Model/Register.php b/src/Model/Register.php index fa8fb7bdb..be00699bf 100644 --- a/src/Model/Register.php +++ b/src/Model/Register.php @@ -21,6 +21,7 @@ namespace Friendica\Model; +use Friendica\Content\Pager; use Friendica\Database\DBA; use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; @@ -33,21 +34,46 @@ class Register /** * Return the list of pending registrations * + * @param int $start Start count (Default is 0) + * @param int $count Count of the items per page (Default is @see Pager::ITEMS_PER_PAGE) + * * @return array * @throws \Exception */ - public static function getPending() + public static function getPending($start = 0, $count = Pager::ITEMS_PER_PAGE) { $stmt = DBA::p( - "SELECT `register`.*, `contact`.`name`, `contact`.`url`, `contact`.`micro`, `user`.`email` + "SELECT `register`.*, `contact`.`name`, `contact`.`url`, `contact`.`micro`, `user`.`email`, `contact`.`nick` FROM `register` INNER JOIN `contact` ON `register`.`uid` = `contact`.`uid` - INNER JOIN `user` ON `register`.`uid` = `user`.`uid`" + INNER JOIN `user` ON `register`.`uid` = `user`.`uid` + LIMIT ?, ?", $start, $count ); return DBA::toArray($stmt); } + /** + * Returns the pending user based on a given user id + * + * @param int $uid The user id + * + * @return array The pending user information + * + * @throws \Exception + */ + public static function getPendingForUser(int $uid) + { + return DBA::fetchFirst( + "SELECT `register`.*, `contact`.`name`, `contact`.`url`, `contact`.`micro`, `user`.`email` + FROM `register` + INNER JOIN `contact` ON `register`.`uid` = `contact`.`uid` + INNER JOIN `user` ON `register`.`uid` = `user`.`uid` + WHERE `register`.uid = ?", + $uid + ); + } + /** * Returns the pending registration count * diff --git a/src/Model/Term.php b/src/Model/Term.php index 6e92c9ce1..868f2bf05 100644 --- a/src/Model/Term.php +++ b/src/Model/Term.php @@ -82,7 +82,7 @@ class Term WHERE `thread`.`visible` AND NOT `thread`.`deleted` AND NOT `thread`.`moderated` - AND NOT `thread`.`private` + AND `thread`.`private` = ? AND t.`uid` = 0 AND t.`otype` = ? AND t.`type` = ? @@ -91,6 +91,7 @@ class Term GROUP BY `term` ORDER BY `score` DESC LIMIT ?", + Item::PUBLIC, Term::OBJECT_TYPE_POST, Term::HASHTAG, $period, @@ -122,11 +123,10 @@ class Term FROM `term` t JOIN `item` i ON i.`id` = t.`oid` AND i.`uid` = t.`uid` JOIN `thread` ON `thread`.`iid` = i.`id` - JOIN `user` ON `user`.`uid` = `thread`.`uid` AND NOT `user`.`hidewall` WHERE `thread`.`visible` AND NOT `thread`.`deleted` AND NOT `thread`.`moderated` - AND NOT `thread`.`private` + AND `thread`.`private` = ? AND `thread`.`wall` AND `thread`.`origin` AND t.`otype` = ? @@ -136,6 +136,7 @@ class Term GROUP BY `term` ORDER BY `score` DESC LIMIT ?", + Item::PUBLIC, Term::OBJECT_TYPE_POST, Term::HASHTAG, $period, @@ -462,13 +463,13 @@ class Term $item['body'] = str_replace($orig_tag, $tag['url'], $item['body']); } - $return['hashtags'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; - $return['tags'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; + $return['hashtags'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; + $return['tags'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; break; case self::MENTION: $tag['url'] = Contact::magicLink($tag['url']); - $return['mentions'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; - $return['tags'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; + $return['mentions'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; + $return['tags'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; break; case self::IMPLICIT_MENTION: $return['implicit_mentions'][] = $prefix . $tag['term']; diff --git a/src/Model/User.php b/src/Model/User.php index e4ef07e47..351982e8a 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -23,7 +23,9 @@ namespace Friendica\Model; use DivineOmega\PasswordExposed; use Exception; +use Friendica\Content\Pager; use Friendica\Core\Hook; +use Friendica\Core\L10n; use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\System; @@ -31,6 +33,7 @@ use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\TwoFactor\AppSpecificPassword; +use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Object\Image; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; @@ -279,7 +282,7 @@ class User * @param string $network network name * * @return int group id - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws InternalServerErrorException */ public static function getDefaultGroup($uid, $network = '') { @@ -556,7 +559,7 @@ class User * * @param string $nickname The nickname that should be checked * @return boolean True is the nickname is blocked on the node - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws InternalServerErrorException */ public static function isNicknameBlocked($nickname) { @@ -593,7 +596,7 @@ class User * @param array $data * @return array * @throws \ErrorException - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws InternalServerErrorException * @throws \ImagickException * @throws Exception */ @@ -880,6 +883,166 @@ class User return $return; } + /** + * Sets block state for a given user + * + * @param int $uid The user id + * @param bool $block Block state (default is true) + * + * @return bool True, if successfully blocked + + * @throws Exception + */ + public static function block(int $uid, bool $block = true) + { + return DBA::update('user', ['blocked' => $block], ['uid' => $uid]); + } + + /** + * Allows a registration based on a hash + * + * @param string $hash + * + * @return bool True, if the allow was successful + * + * @throws InternalServerErrorException + * @throws Exception + */ + public static function allow(string $hash) + { + $register = Register::getByHash($hash); + if (!DBA::isResult($register)) { + return false; + } + + $user = User::getById($register['uid']); + if (!DBA::isResult($user)) { + return false; + } + + Register::deleteByHash($hash); + + DBA::update('user', ['blocked' => false, 'verified' => true], ['uid' => $register['uid']]); + + $profile = DBA::selectFirst('profile', ['net-publish'], ['uid' => $register['uid']]); + + if (DBA::isResult($profile) && $profile['net-publish'] && DI::config()->get('system', 'directory')) { + $url = DI::baseUrl() . '/profile/' . $user['nickname']; + Worker::add(PRIORITY_LOW, "Directory", $url); + } + + $l10n = DI::l10n()->withLang($register['language']); + + return User::sendRegisterOpenEmail( + $l10n, + $user, + DI::config()->get('config', 'sitename'), + DI::baseUrl()->get(), + ($register['password'] ?? '') ?: 'Sent in a previous email' + ); + } + + /** + * Denys a pending registration + * + * @param string $hash The hash of the pending user + * + * This does not have to go through user_remove() and save the nickname + * permanently against re-registration, as the person was not yet + * allowed to have friends on this system + * + * @return bool True, if the deny was successfull + * @throws Exception + */ + public static function deny(string $hash) + { + $register = Register::getByHash($hash); + if (!DBA::isResult($register)) { + return false; + } + + $user = User::getById($register['uid']); + if (!DBA::isResult($user)) { + return false; + } + + return DBA::delete('user', ['uid' => $register['uid']]) && + Register::deleteByHash($register['hash']); + } + + /** + * Creates a new user based on a minimal set and sends an email to this user + * + * @param string $name The user's name + * @param string $email The user's email address + * @param string $nick The user's nick name + * @param string $lang The user's language (default is english) + * + * @return bool True, if the user was created successfully + * @throws InternalServerErrorException + * @throws \ErrorException + * @throws \ImagickException + */ + public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT) + { + if (empty($name) || + empty($email) || + empty($nick)) { + throw new InternalServerErrorException('Invalid arguments.'); + } + + $result = self::create([ + 'username' => $name, + 'email' => $email, + 'nickname' => $nick, + 'verified' => 1, + 'language' => $lang + ]); + + $user = $result['user']; + $preamble = Strings::deindent(DI::l10n()->t(' + Dear %1$s, + the administrator of %2$s has set up an account for you.')); + $body = Strings::deindent(DI::l10n()->t(' + The login details are as follows: + + Site Location: %1$s + Login Name: %2$s + Password: %3$s + + You may change your password from your account "Settings" page after logging + in. + + Please take a few moments to review the other account settings on that page. + + You may also wish to add some basic information to your default profile + (on the "Profiles" page) so that other people can easily find you. + + We recommend setting your full name, adding a profile photo, + adding some profile "keywords" (very useful in making new friends) - and + perhaps what country you live in; if you do not wish to be more specific + than that. + + We fully respect your right to privacy, and none of these items are necessary. + If you are new and do not know anybody here, they may help + you to make some new and interesting friends. + + If you ever want to delete your account, you can do so at %1$s/removeme + + Thank you and welcome to %4$s.')); + + $preamble = sprintf($preamble, $user['username'], DI::config()->get('config', 'sitename')); + $body = sprintf($body, DI::baseUrl()->get(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename')); + + $email = DI::emailer() + ->newSystemMail() + ->withMessage(DI::l10n()->t('Registration details for %s', DI::config()->get('config', 'sitename')), $preamble, $body) + ->forUser($user) + ->withRecipient($user['email']) + ->build(); + return DI::emailer()->send($email); + } + /** * Sends pending registration confirmation email * @@ -888,7 +1051,7 @@ class User * @param string $siteurl * @param string $password Plaintext password * @return NULL|boolean from notification() and email() inherited - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws InternalServerErrorException */ public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password) { @@ -931,7 +1094,7 @@ class User * @param string $password Plaintext password * * @return NULL|boolean from notification() and email() inherited - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws InternalServerErrorException */ public static function sendRegisterOpenEmail(\Friendica\Core\L10n $l10n, $user, $sitename, $siteurl, $password) { @@ -988,11 +1151,11 @@ class User } /** - * @param object $uid user to remove + * @param int $uid user to remove * @return bool - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws InternalServerErrorException */ - public static function remove($uid) + public static function remove(int $uid) { if (!$uid) { return false; @@ -1154,4 +1317,47 @@ class User return $statistics; } + + /** + * Get all users of the current node + * + * @param int $start Start count (Default is 0) + * @param int $count Count of the items per page (Default is @see Pager::ITEMS_PER_PAGE) + * @param string $type The type of users, which should get (all, bocked, removed) + * @param string $order Order of the user list (Default is 'contact.name') + * @param string $order_direction Order direction (Default is ASC) + * + * @return array The list of the users + * @throws Exception + */ + public static function getList($start = 0, $count = Pager::ITEMS_PER_PAGE, $type = 'all', $order = 'contact.name', $order_direction = '+') + { + $sql_order = '`' . str_replace('.', '`.`', $order) . '`'; + $sql_order_direction = ($order_direction === '+') ? 'ASC' : 'DESC'; + + switch ($type) { + case 'active': + $sql_extra = 'AND `user`.`blocked` = 0'; + break; + case 'blocked': + $sql_extra = 'AND `user`.`blocked` = 1'; + break; + case 'removed': + $sql_extra = 'AND `user`.`account_removed` = 1'; + break; + case 'all': + default: + $sql_extra = ''; + break; + } + + $usersStmt = DBA::p("SELECT `user`.*, `contact`.`name`, `contact`.`url`, `contact`.`micro`, `user`.`account_expired`, `contact`.`last-item` AS `lastitem_date`, `contact`.`nick`, `contact`.`created` + FROM `user` + INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self` + WHERE `user`.`verified` $sql_extra + ORDER BY $sql_order $sql_order_direction LIMIT ?, ?", $start, $count + ); + + return DBA::toArray($usersStmt); + } } diff --git a/src/Module/Admin/Features.php b/src/Module/Admin/Features.php index 62136f423..46c0a1384 100644 --- a/src/Module/Admin/Features.php +++ b/src/Module/Admin/Features.php @@ -73,8 +73,8 @@ class Features extends BaseAdmin foreach (array_slice($fdata, 1) as $f) { $set = DI::config()->get('feature', $f[0], $f[3]); $arr[$fname][1][] = [ - ['feature_' . $f[0], $f[1], $set, $f[2], [DI::l10n()->t('Off'), DI::l10n()->t('On')]], - ['featurelock_' . $f[0], DI::l10n()->t('Lock feature %s', $f[1]), (($f[4] !== false) ? "1" : ''), '', [DI::l10n()->t('Off'), DI::l10n()->t('On')]] + ['feature_' . $f[0], $f[1], $set, $f[2]], + ['featurelock_' . $f[0], DI::l10n()->t('Lock feature %s', $f[1]), $f[4], ''] ]; } } diff --git a/src/Module/Admin/Tos.php b/src/Module/Admin/Tos.php index c9dd3d879..811a0eb25 100644 --- a/src/Module/Admin/Tos.php +++ b/src/Module/Admin/Tos.php @@ -60,7 +60,7 @@ class Tos extends BaseAdmin '$title' => DI::l10n()->t('Administration'), '$page' => DI::l10n()->t('Terms of Service'), '$displaytos' => ['displaytos', DI::l10n()->t('Display Terms of Service'), DI::config()->get('system', 'tosdisplay'), DI::l10n()->t('Enable the Terms of Service page. If this is enabled a link to the terms will be added to the registration form and the general information page.')], - '$displayprivstatement' => ['displayprivstatement', DI::l10n()->t('Display Privacy Statement'), DI::config()->get('system', 'tosprivstatement'), DI::l10n()->t('Show some informations regarding the needed information to operate the node according e.g. to EU-GDPR.', 'https://en.wikipedia.org/wiki/General_Data_Protection_Regulation')], + '$displayprivstatement' => ['displayprivstatement', DI::l10n()->t('Display Privacy Statement'), DI::config()->get('system', 'tosprivstatement'), DI::l10n()->t('Show some informations regarding the needed information to operate the node according e.g. to EU-GDPR.', 'https://en.wikipedia.org/wiki/General_Data_Protection_Regulation')], '$preview' => DI::l10n()->t('Privacy Statement Preview'), '$privtext' => $tos->privacy_complete, '$tostext' => ['tostext', DI::l10n()->t('The Terms of Service'), DI::config()->get('system', 'tostext'), DI::l10n()->t('Enter the Terms of Service for your node here. You can use BBCode. Headers of sections should be [h2] and below.')], diff --git a/src/Module/Admin/Users.php b/src/Module/Admin/Users.php index b446a2c47..3ef91aadf 100644 --- a/src/Module/Admin/Users.php +++ b/src/Module/Admin/Users.php @@ -28,7 +28,6 @@ use Friendica\DI; use Friendica\Model\Register; use Friendica\Model\User; use Friendica\Module\BaseAdmin; -use Friendica\Util\Strings; use Friendica\Util\Temporal; class Users extends BaseAdmin @@ -48,71 +47,24 @@ class Users extends BaseAdmin if ($nu_name !== '' && $nu_email !== '' && $nu_nickname !== '') { try { - $result = User::create([ - 'username' => $nu_name, - 'email' => $nu_email, - 'nickname' => $nu_nickname, - 'verified' => 1, - 'language' => $nu_language - ]); + User::createMinimal($nu_name, $nu_email, $nu_nickname, $nu_language); } catch (\Exception $ex) { notice($ex->getMessage()); return; } - - $user = $result['user']; - $preamble = Strings::deindent(DI::l10n()->t(' - Dear %1$s, - the administrator of %2$s has set up an account for you.')); - $body = Strings::deindent(DI::l10n()->t(' - The login details are as follows: - - Site Location: %1$s - Login Name: %2$s - Password: %3$s - - You may change your password from your account "Settings" page after logging - in. - - Please take a few moments to review the other account settings on that page. - - You may also wish to add some basic information to your default profile - (on the "Profiles" page) so that other people can easily find you. - - We recommend setting your full name, adding a profile photo, - adding some profile "keywords" (very useful in making new friends) - and - perhaps what country you live in; if you do not wish to be more specific - than that. - - We fully respect your right to privacy, and none of these items are necessary. - If you are new and do not know anybody here, they may help - you to make some new and interesting friends. - - If you ever want to delete your account, you can do so at %1$s/removeme - - Thank you and welcome to %4$s.')); - - $preamble = sprintf($preamble, $user['username'], DI::config()->get('config', 'sitename')); - $body = sprintf($body, DI::baseUrl()->get(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename')); - - $email = DI::emailer() - ->newSystemMail() - ->withMessage(DI::l10n()->t('Registration details for %s', DI::config()->get('config', 'sitename')), $preamble, $body) - ->forUser($user) - ->withRecipient($user['email']) - ->build(); - return DI::emailer()->send($email); } if (!empty($_POST['page_users_block'])) { - // @TODO Move this to Model\User:block($users); - DBA::update('user', ['blocked' => 1], ['uid' => $users]); + foreach ($users as $uid) { + User::block($uid); + } notice(DI::l10n()->tt('%s user blocked', '%s users blocked', count($users))); } if (!empty($_POST['page_users_unblock'])) { - // @TODO Move this to Model\User:unblock($users); - DBA::update('user', ['blocked' => 0], ['uid' => $users]); + foreach ($users as $uid) { + User::block($uid, false); + } notice(DI::l10n()->tt('%s user unblocked', '%s users unblocked', count($users))); } @@ -129,17 +81,17 @@ class Users extends BaseAdmin } if (!empty($_POST['page_users_approve'])) { - require_once 'mod/regmod.php'; foreach ($pending as $hash) { - user_allow($hash); + User::allow($hash); } + notice(DI::l10n()->tt('%s user approved', '%s users approved', count($pending))); } if (!empty($_POST['page_users_deny'])) { - require_once 'mod/regmod.php'; foreach ($pending as $hash) { - user_deny($hash); + User::deny($hash); } + notice(DI::l10n()->tt('%s registration revoked', '%s registrations revoked', count($pending))); } DI::baseUrl()->redirect('admin/users'); @@ -176,16 +128,24 @@ class Users extends BaseAdmin break; case 'block': parent::checkFormSecurityTokenRedirectOnError('/admin/users', 'admin_users', 't'); - // @TODO Move this to Model\User:block([$uid]); - DBA::update('user', ['blocked' => 1], ['uid' => $uid]); + User::block($uid); notice(DI::l10n()->t('User "%s" blocked', $user['username'])); break; case 'unblock': parent::checkFormSecurityTokenRedirectOnError('/admin/users', 'admin_users', 't'); - // @TODO Move this to Model\User:unblock([$uid]); - DBA::update('user', ['blocked' => 0], ['uid' => $uid]); + User::block($uid, false); notice(DI::l10n()->t('User "%s" unblocked', $user['username'])); break; + case 'allow': + parent::checkFormSecurityTokenRedirectOnError('/admin/users', 'admin_users', 't'); + User::allow(Register::getPendingForUser($uid)['hash'] ?? ''); + notice(DI::l10n()->t('Account approved.')); + break; + case 'deny': + parent::checkFormSecurityTokenRedirectOnError('/admin/users', 'admin_users', 't'); + User::deny(Register::getPendingForUser($uid)['hash'] ?? ''); + notice(DI::l10n()->t('Registration revoked')); + break; } DI::baseUrl()->redirect('admin/users'); @@ -196,7 +156,6 @@ class Users extends BaseAdmin $pager = new Pager(DI::l10n(), DI::args()->getQueryString(), 100); - // @TODO Move below block to Model\User::getUsers($start, $count, $order = 'contact.name', $order_direction = '+') $valid_orders = [ 'contact.name', 'user.email', @@ -219,16 +178,8 @@ class Users extends BaseAdmin $order = $new_order; } } - $sql_order = '`' . str_replace('.', '`.`', $order) . '`'; - $sql_order_direction = ($order_direction === '+') ? 'ASC' : 'DESC'; - $usersStmt = DBA::p("SELECT `user`.*, `contact`.`name`, `contact`.`url`, `contact`.`micro`, `user`.`account_expired`, `contact`.`last-item` AS `lastitem_date` - FROM `user` - INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self` - WHERE `user`.`verified` - ORDER BY $sql_order $sql_order_direction LIMIT ?, ?", $pager->getStart(), $pager->getItemsPerPage() - ); - $users = DBA::toArray($usersStmt); + $users = User::getList($pager->getStart(), $pager->getItemsPerPage(), 'all', $order, $order_direction); $adminlist = explode(',', str_replace(' ', '', DI::config()->get('config', 'admin_email'))); $_setup_users = function ($e) use ($adminlist) { @@ -283,7 +234,7 @@ class Users extends BaseAdmin } } - $th_users = array_map(null, [DI::l10n()->t('Name'), DI::l10n()->t('Email'), DI::l10n()->t('Register date'), DI::l10n()->t('Last login'), DI::l10n()->t('Last item'), DI::l10n()->t('Type')], $valid_orders); + $th_users = array_map(null, [DI::l10n()->t('Name'), DI::l10n()->t('Email'), DI::l10n()->t('Register date'), DI::l10n()->t('Last login'), DI::l10n()->t('Last public item'), DI::l10n()->t('Type')], $valid_orders); $t = Renderer::getMarkupTemplate('admin/users.tpl'); $o = Renderer::replaceMacros($t, [ @@ -308,7 +259,7 @@ class Users extends BaseAdmin '$h_users' => DI::l10n()->t('Users'), '$h_newuser' => DI::l10n()->t('New User'), - '$th_deleted' => [DI::l10n()->t('Name'), DI::l10n()->t('Email'), DI::l10n()->t('Register date'), DI::l10n()->t('Last login'), DI::l10n()->t('Last item'), DI::l10n()->t('Permanent deletion')], + '$th_deleted' => [DI::l10n()->t('Name'), DI::l10n()->t('Email'), DI::l10n()->t('Register date'), DI::l10n()->t('Last login'), DI::l10n()->t('Last public item'), DI::l10n()->t('Permanent deletion')], '$th_users' => $th_users, '$order_users' => $order, '$order_direction_users' => $order_direction, diff --git a/src/Module/Conversation/Community.php b/src/Module/Conversation/Community.php index 58af38eb1..5637c6f41 100644 --- a/src/Module/Conversation/Community.php +++ b/src/Module/Conversation/Community.php @@ -280,58 +280,33 @@ class Community extends BaseModule $r = false; if (self::$content == 'local') { - $values = []; - - $sql_accounttype = ''; - $sql_boundaries = ''; if (!is_null(self::$accounttype)) { - $sql_accounttype = " AND `user`.`account-type` = ?"; - $values[] = [self::$accounttype]; + $condition = ["`wall` AND `origin` AND `private` = ? AND `owner`.`contact-type` = ?", Item::PUBLIC, self::$accounttype]; + } else { + $condition = ["`wall` AND `origin` AND `private` = ?", Item::PUBLIC]; } - - if (isset($since_id)) { - $sql_boundaries .= " AND `thread`.`commented` > ?"; - $values[] = $since_id; - } - - if (isset($max_id)) { - $sql_boundaries .= " AND `thread`.`commented` < ?"; - $values[] = $max_id; - } - - $values[] = $itemspage; - - /// @todo Use "unsearchable" here as well (instead of "hidewall") - $r = DBA::p("SELECT `item`.`uri`, `author`.`url` AS `author-link`, `thread`.`commented` FROM `thread` - STRAIGHT_JOIN `user` ON `user`.`uid` = `thread`.`uid` AND NOT `user`.`hidewall` - STRAIGHT_JOIN `item` ON `item`.`id` = `thread`.`iid` - STRAIGHT_JOIN `contact` AS `author` ON `author`.`id`=`item`.`author-id` - WHERE `thread`.`visible` AND NOT `thread`.`deleted` AND NOT `thread`.`moderated` - AND NOT `thread`.`private` AND `thread`.`wall` AND `thread`.`origin` - $sql_accounttype - $sql_boundaries - ORDER BY `thread`.`commented` DESC - LIMIT ?", $values); } elseif (self::$content == 'global') { if (!is_null(self::$accounttype)) { - $condition = ["`uid` = ? AND NOT `author`.`unsearchable` AND NOT `owner`.`unsearchable` AND `owner`.`contact-type` = ?", 0, self::$accounttype]; + $condition = ["`uid` = ? AND `private` = ? AND `owner`.`contact-type` = ?", 0, Item::PUBLIC, self::$accounttype]; } else { - $condition = ["`uid` = ? AND NOT `author`.`unsearchable` AND NOT `owner`.`unsearchable`", 0]; + $condition = ["`uid` = ? AND `private` = ?", 0, Item::PUBLIC]; } - - if (isset($max_id)) { - $condition[0] .= " AND `commented` < ?"; - $condition[] = $max_id; - } - - if (isset($since_id)) { - $condition[0] .= " AND `commented` > ?"; - $condition[] = $since_id; - } - - $r = Item::selectThreadForUser(0, ['uri', 'commented'], $condition, ['order' => ['commented' => true], 'limit' => $itemspage]); + } else { + return []; } + if (isset($max_id)) { + $condition[0] .= " AND `commented` < ?"; + $condition[] = $max_id; + } + + if (isset($since_id)) { + $condition[0] .= " AND `commented` > ?"; + $condition[] = $since_id; + } + + $r = Item::selectThreadForUser(0, ['uri', 'commented', 'author-link'], $condition, ['order' => ['commented' => true], 'limit' => $itemspage]); + return DBA::toArray($r); } } diff --git a/src/Module/Diaspora/Fetch.php b/src/Module/Diaspora/Fetch.php index 70f982f70..aba9d33be 100644 --- a/src/Module/Diaspora/Fetch.php +++ b/src/Module/Diaspora/Fetch.php @@ -54,7 +54,7 @@ class Fetch extends BaseModule 'uid', 'title', 'body', 'guid', 'contact-id', 'private', 'created', 'received', 'app', 'location', 'coord', 'network', 'event-id', 'resource-id', 'author-link', 'author-avatar', 'author-name', 'plink', 'owner-link', 'attach' ]; - $condition = ['wall' => true, 'private' => false, 'guid' => $guid, 'network' => [Protocol::DFRN, Protocol::DIASPORA]]; + $condition = ['wall' => true, 'private' => [Item::PUBLIC, Item::UNLISTED], 'guid' => $guid, 'network' => [Protocol::DFRN, Protocol::DIASPORA]]; $item = Item::selectFirst($fields, $condition); if (empty($item)) { $condition = ['guid' => $guid, 'network' => [Protocol::DFRN, Protocol::DIASPORA]]; diff --git a/src/Module/Notifications/Notification.php b/src/Module/Notifications/Notification.php index 2f5cfa869..2dc008248 100644 --- a/src/Module/Notifications/Notification.php +++ b/src/Module/Notifications/Notification.php @@ -24,6 +24,7 @@ namespace Friendica\Module\Notifications; use Friendica\BaseModule; use Friendica\Core\System; use Friendica\DI; +use Friendica\Module\Security\Login; use Friendica\Network\HTTPException; /** @@ -31,15 +32,21 @@ use Friendica\Network\HTTPException; */ class Notification extends BaseModule { - public static function init(array $parameters = []) + /** + * {@inheritDoc} + * + * @throws HTTPException\InternalServerErrorException + * @throws HTTPException\NotFoundException + * @throws HTTPException\UnauthorizedException + * @throws \ImagickException + * @throws \Exception + */ + public static function post(array $parameters = []) { if (!local_user()) { throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.')); } - } - public static function post(array $parameters = []) - { $request_id = $parameters['id'] ?? false; if ($request_id) { @@ -58,9 +65,17 @@ class Notification extends BaseModule } } + /** + * {@inheritDoc} + * + * @throws HTTPException\UnauthorizedException + */ public static function rawContent(array $parameters = []) { - // @TODO: Replace with parameter from router + if (!local_user()) { + throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.')); + } + if (DI::args()->get(1) === 'mark' && DI::args()->get(2) === 'all') { try { $success = DI::notify()->setSeen(); @@ -74,31 +89,36 @@ class Notification extends BaseModule } /** + * {@inheritDoc} + * * Redirect to the notifications main page or to the url for the chosen notifications * - * @return string|void + * @throws HTTPException\NotFoundException In case the notification is either not existing or is not for this user * @throws HTTPException\InternalServerErrorException + * @throws \Exception */ public static function content(array $parameters = []) { + if (!local_user()) { + notice(DI::l10n()->t('You must be logged in to show this page.')); + return Login::form(); + } + $request_id = $parameters['id'] ?? false; if ($request_id) { - try { - $notify = DI::notify()->getByID($request_id); - DI::notify()->setSeen(true, $notify); + $notify = DI::notify()->getByID($request_id, local_user()); + DI::notify()->setSeen(true, $notify); - if (!empty($notify->link)) { - System::externalRedirect($notify->link); - } - - } catch (HTTPException\NotFoundException $e) { - info(DI::l10n()->t('Invalid notification.')); + if (!empty($notify->link)) { + System::externalRedirect($notify->link); } DI::baseUrl()->redirect(); } DI::baseUrl()->redirect('notifications/system'); + + throw new HTTPException\InternalServerErrorException('Invalid situation.'); } } diff --git a/src/Module/Objects.php b/src/Module/Objects.php index 9c57665ff..023ced08c 100644 --- a/src/Module/Objects.php +++ b/src/Module/Objects.php @@ -49,11 +49,11 @@ class Objects extends BaseModule // At first we try the original post with that guid // @TODO: Replace with parameter from router - $item = Item::selectFirst(['id'], ['guid' => $a->argv[1], 'origin' => true, 'private' => false]); + $item = Item::selectFirst(['id'], ['guid' => $a->argv[1], 'origin' => true, 'private' => [item::PRIVATE, Item::UNLISTED]]); if (!DBA::isResult($item)) { // If no original post could be found, it could possibly be a forum post, there we remove the "origin" field. // @TODO: Replace with parameter from router - $item = Item::selectFirst(['id', 'author-link'], ['guid' => $a->argv[1], 'private' => false]); + $item = Item::selectFirst(['id', 'author-link'], ['guid' => $a->argv[1], 'private' => [item::PRIVATE, Item::UNLISTED]]); if (!DBA::isResult($item) || !strstr($item['author-link'], DI::baseUrl()->get())) { throw new \Friendica\Network\HTTPException\NotFoundException(); } diff --git a/src/Module/Settings/Display.php b/src/Module/Settings/Display.php index 644453756..bde049718 100644 --- a/src/Module/Settings/Display.php +++ b/src/Module/Settings/Display.php @@ -197,7 +197,7 @@ class Display extends BaseSettings '$itemspage_network' => ['itemspage_network' , DI::l10n()->t('Number of items to display per page:'), $itemspage_network, DI::l10n()->t('Maximum of 100 items')], '$itemspage_mobile_network' => ['itemspage_mobile_network', DI::l10n()->t('Number of items to display per page when viewed from mobile device:'), $itemspage_mobile_network, DI::l10n()->t('Maximum of 100 items')], '$ajaxint' => ['browser_update' , DI::l10n()->t('Update browser every xx seconds'), $browser_update, DI::l10n()->t('Minimum of 10 seconds. Enter -1 to disable it.')], - '$no_auto_update' => ['no_auto_update' , DI::l10n()->t('Automatic updates only at the top of the network page'), $no_auto_update, DI::l10n()->t('When disabled, the network page is updated all the time, which could be confusing while reading.')], + '$no_auto_update' => ['no_auto_update' , DI::l10n()->t('Automatic updates only at the top of the post stream pages'), $no_auto_update, DI::l10n()->t('Auto update may add new posts at the top of the post stream pages, which can affect the scroll position and perturb normal reading if it happens anywhere else the top of the page.')], '$nosmile' => ['nosmile' , DI::l10n()->t('Don\'t show emoticons'), $nosmile, DI::l10n()->t('Normally emoticons are replaced with matching symbols. This setting disables this behaviour.')], '$infinite_scroll' => ['infinite_scroll' , DI::l10n()->t('Infinite scroll'), $infinite_scroll, DI::l10n()->t('Automatic fetch new items when reaching the page end.')], '$no_smart_threading' => ['no_smart_threading' , DI::l10n()->t('Disable Smart Threading'), $no_smart_threading, DI::l10n()->t('Disable the automatic suppression of extraneous thread indentation.')], diff --git a/src/Module/Update/Community.php b/src/Module/Update/Community.php index e0bc6c067..b064b4e86 100644 --- a/src/Module/Update/Community.php +++ b/src/Module/Update/Community.php @@ -37,7 +37,10 @@ class Community extends CommunityModule { self::parseRequest($parameters); - $o = conversation(DI::app(), self::getItems(), 'community', true, false, 'commented', local_user()); + $o = ''; + if (!empty($_GET['force']) || !DI::pConfig()->get(local_user(), 'system', 'no_auto_update')) { + $o = conversation(DI::app(), self::getItems(), 'community', true, false, 'commented', local_user()); + } System::htmlUpdateExit($o); } diff --git a/src/Module/Update/Profile.php b/src/Module/Update/Profile.php index 662042eb1..38ef3b09e 100644 --- a/src/Module/Update/Profile.php +++ b/src/Module/Update/Profile.php @@ -42,8 +42,6 @@ class Profile extends BaseModule throw new ForbiddenException(); } - $o = ''; - $profile_uid = intval($_GET['p'] ?? 0); // Ensure we've got a profile owner if updating. @@ -57,6 +55,12 @@ class Profile extends BaseModule throw new ForbiddenException(DI::l10n()->t('Access to this profile has been restricted.')); } + $o = ''; + + if (empty($_GET['force']) && DI::pConfig()->get(local_user(), 'system', 'no_auto_update')) { + System::htmlUpdateExit($o); + } + // Get permissions SQL - if $remote_contact is true, our remote user has been pre-verified and we already have fetched his/her groups $sql_extra = Item::getPermissionsSQLByUserId($a->profile['uid']); diff --git a/src/Object/Post.php b/src/Object/Post.php index c92c5a32b..1c1f85e2a 100644 --- a/src/Object/Post.php +++ b/src/Object/Post.php @@ -170,12 +170,12 @@ class Post $conv = $this->getThread(); - $lock = ((($item['private'] == 1) || (($item['uid'] == local_user()) && (strlen($item['allow_cid']) || strlen($item['allow_gid']) + $lock = ((($item['private'] == Item::PRIVATE) || (($item['uid'] == local_user()) && (strlen($item['allow_cid']) || strlen($item['allow_gid']) || strlen($item['deny_cid']) || strlen($item['deny_gid'])))) ? DI::l10n()->t('Private Message') : false); - $shareable = in_array($conv->getProfileOwner(), [0, local_user()]) && $item['private'] != 1; + $shareable = in_array($conv->getProfileOwner(), [0, local_user()]) && $item['private'] != Item::PRIVATE; $edpost = false; @@ -272,10 +272,12 @@ class Post } } - $responses = get_responses($conv_responses, $response_verbs, $item, $this); - - foreach ($response_verbs as $value => $verbs) { - $responses[$verbs]['output'] = !empty($conv_responses[$verbs][$item['uri']]) ? format_like($conv_responses[$verbs][$item['uri']], $conv_responses[$verbs][$item['uri'] . '-l'], $verbs, $item['uri']) : ''; + $responses = []; + foreach ($response_verbs as $value => $verb) { + $responses[$verb] = [ + 'self' => $conv_responses[$verb][$item['uri'] . '-self'] ?? 0, + 'output' => !empty($conv_responses[$verb][$item['uri']]) ? format_like($conv_responses[$verb][$item['uri']], $conv_responses[$verb][$item['uri'] . '-l'], $verb, $item['uri']) : '', + ]; } /* diff --git a/src/Object/Thread.php b/src/Object/Thread.php index b574e300b..f62b14c71 100644 --- a/src/Object/Thread.php +++ b/src/Object/Thread.php @@ -34,6 +34,7 @@ use Friendica\Util\Security; */ class Thread { + /** @var Post[] */ private $parents = []; private $mode = null; private $writable = false; diff --git a/src/Protocol/ActivityPub/Processor.php b/src/Protocol/ActivityPub/Processor.php index 65f101746..6d6c652fd 100644 --- a/src/Protocol/ActivityPub/Processor.php +++ b/src/Protocol/ActivityPub/Processor.php @@ -29,6 +29,7 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\APContact; use Friendica\Model\Contact; +use Friendica\Model\Conversation; use Friendica\Model\Event; use Friendica\Model\Item; use Friendica\Model\Mail; @@ -375,7 +376,7 @@ class Processor Logger::warning('Unknown parent item.', ['uri' => $item['thr-parent']]); return false; } - if ($item_private && !$parent['private']) { + if ($item_private && ($parent['private'] == Item::PRIVATE)) { Logger::warning('Item is private but the parent is not. Dropping.', ['item-uri' => $item['uri'], 'thr-parent' => $item['thr-parent']]); return false; } @@ -442,12 +443,26 @@ class Processor } $item['network'] = Protocol::ACTIVITYPUB; - $item['private'] = !in_array(0, $activity['receiver']); $item['author-link'] = $activity['author']; $item['author-id'] = Contact::getIdForURL($activity['author'], 0, true); $item['owner-link'] = $activity['actor']; $item['owner-id'] = Contact::getIdForURL($activity['actor'], 0, true); + if (in_array(0, $activity['receiver']) && !empty($activity['unlisted'])) { + $item['private'] = Item::UNLISTED; + } elseif (in_array(0, $activity['receiver'])) { + $item['private'] = Item::PUBLIC; + } else { + $item['private'] = Item::PRIVATE; + } + + if (!empty($activity['raw'])) { + $item['source'] = $activity['raw']; + $item['protocol'] = Conversation::PARCEL_ACTIVITYPUB; + $item['conversation-href'] = $activity['context'] ?? ''; + $item['conversation-uri'] = $activity['conversation'] ?? ''; + } + $isForum = false; if (!empty($activity['thread-completion'])) { @@ -490,6 +505,10 @@ class Processor $stored = false; foreach ($activity['receiver'] as $receiver) { + if ($receiver == -1) { + continue; + } + $item['uid'] = $receiver; if ($isForum) { @@ -539,7 +558,7 @@ class Processor } // Store send a follow request for every reshare - but only when the item had been stored - if ($stored && !$item['private'] && ($item['gravity'] == GRAVITY_PARENT) && ($item['author-link'] != $item['owner-link'])) { + if ($stored && ($item['private'] != Item::PRIVATE) && ($item['gravity'] == GRAVITY_PARENT) && ($item['author-link'] != $item['owner-link'])) { $author = APContact::getByURL($item['owner-link'], false); // We send automatic follow requests for reshared messages. (We don't need though for forum posts) if ($author['type'] != 'Group') { diff --git a/src/Protocol/ActivityPub/Receiver.php b/src/Protocol/ActivityPub/Receiver.php index 96e1588d4..39655f45d 100644 --- a/src/Protocol/ActivityPub/Receiver.php +++ b/src/Protocol/ActivityPub/Receiver.php @@ -28,7 +28,6 @@ use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Model\Contact; use Friendica\Model\APContact; -use Friendica\Model\Conversation; use Friendica\Model\Item; use Friendica\Model\User; use Friendica\Protocol\Activity; @@ -306,33 +305,6 @@ class Receiver return 0; } - /** - * Store the unprocessed data into the conversation table - * This has to be done outside the regular function, - * since we store everything - not only item posts. - * - * @param array $activity Array with activity data - * @param string $body The raw message - * @throws \Exception - */ - private static function storeConversation($activity, $body) - { - if (empty($body) || empty($activity['id'])) { - return; - } - - $conversation = [ - 'protocol' => Conversation::PARCEL_ACTIVITYPUB, - 'item-uri' => $activity['id'], - 'reply-to-uri' => $activity['reply-to-id'] ?? '', - 'conversation-href' => $activity['context'] ?? '', - 'conversation-uri' => $activity['conversation'] ?? '', - 'source' => $body, - 'received' => DateTimeFormat::utcNow()]; - - DBA::insert('conversation', $conversation, true); - } - /** * Processes the activity object * @@ -383,9 +355,8 @@ class Receiver return; } - // Only store content related stuff - and no announces, since they possibly overwrite the original content - if (in_array($object_data['object_type'], self::CONTENT_TYPES) && ($type != 'as:Announce')) { - self::storeConversation($object_data, $body); + if (!empty($body)) { + $object_data['raw'] = $body; } // Internal flag for thread completion. See Processor.php @@ -509,14 +480,15 @@ class Receiver /** * Fetch the receiver list from an activity array * - * @param array $activity - * @param string $actor - * @param array $tags + * @param array $activity + * @param string $actor + * @param array $tags + * @param boolean $fetch_unlisted * * @return array with receivers (user id) * @throws \Exception */ - private static function getReceivers($activity, $actor, $tags = []) + private static function getReceivers($activity, $actor, $tags = [], $fetch_unlisted = false) { $receivers = []; @@ -554,6 +526,11 @@ class Receiver $receivers['uid:0'] = 0; } + // Add receiver "-1" for unlisted posts + if ($fetch_unlisted && ($receiver == self::PUBLIC_COLLECTION) && ($element == 'as:cc')) { + $receivers['uid:-1'] = -1; + } + if (($receiver == self::PUBLIC_COLLECTION) && !empty($actor)) { // This will most likely catch all OStatus connections to Mastodon $condition = ['alias' => [$actor, Strings::normaliseLink($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND] @@ -1025,7 +1002,9 @@ class Receiver } } - $object_data['receiver'] = self::getReceivers($object, $object_data['actor'], $object_data['tags']); + $object_data['receiver'] = self::getReceivers($object, $object_data['actor'], $object_data['tags'], true); + $object_data['unlisted'] = in_array(-1, $object_data['receiver']); + unset($object_data['receiver']['uid:-1']); // Common object data: diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 38f102294..0b80e9786 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -173,7 +173,7 @@ class Transmitter $public_contact = Contact::getIdForURL($owner['url'], 0, true); $condition = ['uid' => 0, 'contact-id' => $public_contact, 'author-id' => $public_contact, - 'private' => false, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], + 'private' => [Item::PUBLIC, Item::UNLISTED], 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'deleted' => false, 'visible' => true, 'moderated' => false]; $count = DBA::count('item', $condition); @@ -264,7 +264,7 @@ class Transmitter $data['name'] = $contact['name']; $data['vcard:hasAddress'] = ['@type' => 'vcard:Home', 'vcard:country-name' => $profile['country-name'], 'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']]; - $data['summary'] = $contact['about']; + $data['summary'] = BBCode::convert($contact['about'], false); $data['url'] = $contact['url']; $data['manuallyApprovesFollowers'] = in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]); $data['publicKey'] = ['id' => $contact['url'] . '#main-key', @@ -401,7 +401,7 @@ class Transmitter $terms = Term::tagArrayFromItemId($item['id'], [Term::MENTION, Term::IMPLICIT_MENTION]); - if (!$item['private']) { + if ($item['private'] != Item::PRIVATE) { // Directly mention the original author upon a quoted reshare. // Else just ensure that the original author receives the reshare. $announce = self::getAnnounceArray($item); @@ -413,7 +413,12 @@ class Transmitter $data = array_merge($data, self::fetchPermissionBlockFromConversation($item)); - $data['to'][] = ActivityPub::PUBLIC_COLLECTION; + // Check if the item is completely public or unlisted + if ($item['private'] == Item::PUBLIC) { + $data['to'][] = ActivityPub::PUBLIC_COLLECTION; + } else { + $data['cc'][] = ActivityPub::PUBLIC_COLLECTION; + } foreach ($terms as $term) { $profile = APContact::getByURL($term['url'], false); @@ -467,13 +472,13 @@ class Transmitter $data['to'][] = $profile['url']; } else { $data['cc'][] = $profile['url']; - if (!$item['private'] && !empty($actor_profile['followers'])) { + if (($item['private'] != Item::PRIVATE) && $item['private'] && !empty($actor_profile['followers'])) { $data['cc'][] = $actor_profile['followers']; } } } else { // Public thread parent post always are directed to the followers - if (!$item['private'] && !$forum_mode) { + if (($item['private'] != Item::PRIVATE) && !$forum_mode) { $data['cc'][] = $actor_profile['followers']; } } diff --git a/src/Protocol/DFRN.php b/src/Protocol/DFRN.php index f065cd67e..4c88db1d9 100644 --- a/src/Protocol/DFRN.php +++ b/src/Protocol/DFRN.php @@ -182,7 +182,7 @@ class DFRN // default permissions - anonymous user - $sql_extra = " AND NOT `item`.`private` "; + $sql_extra = sprintf(" AND `item`.`private` != %s ", Item::PRIVATE); $r = q( "SELECT `contact`.*, `user`.`nickname`, `user`.`timezone`, `user`.`page-flags`, `user`.`account-type` @@ -234,7 +234,7 @@ class DFRN if (!empty($set)) { $sql_extra = " AND `item`.`psid` IN (" . implode(',', $set) .")"; } else { - $sql_extra = " AND NOT `item`.`private`"; + $sql_extra = sprintf(" AND `item`.`private` != %s", Item::PRIVATE); } } @@ -332,7 +332,7 @@ class DFRN if ($public_feed) { $type = 'html'; // catch any email that's in a public conversation and make sure it doesn't leak - if ($item['private']) { + if ($item['private'] == Item::PRIVATE) { continue; } } else { @@ -955,7 +955,7 @@ class DFRN $entry->setAttribute("xmlns:statusnet", ActivityNamespace::STATUSNET); } - if ($item['private']) { + if ($item['private'] == Item::PRIVATE) { $body = Item::fixPrivatePhotos($item['body'], $owner['uid'], $item, $cid); } else { $body = $item['body']; @@ -1050,7 +1050,9 @@ class DFRN } if ($item['private']) { - XML::addElement($doc, $entry, "dfrn:private", ($item['private'] ? $item['private'] : 1)); + // Friendica versions prior to 2020.3 can't handle "unlisted" properly. So we can only transmit public and private + XML::addElement($doc, $entry, "dfrn:private", ($item['private'] == Item::PRIVATE ? Item::PRIVATE : Item::PUBLIC)); + XML::addElement($doc, $entry, "dfrn:unlisted", $item['private'] == Item::UNLISTED); } if ($item['extid']) { @@ -2404,6 +2406,11 @@ class DFRN $item["private"] = XML::getFirstNodeValue($xpath, "dfrn:private/text()", $entry); + $unlisted = XML::getFirstNodeValue($xpath, "dfrn:unlisted/text()", $entry); + if (!empty($unlisted) && ($item['private'] != Item::PRIVATE)) { + $item['private'] = Item::UNLISTED; + } + $item["extid"] = XML::getFirstNodeValue($xpath, "dfrn:extid/text()", $entry); if (XML::getFirstNodeValue($xpath, "dfrn:bookmark/text()", $entry) == "true") { diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index fd2099110..cda428021 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -2211,7 +2211,7 @@ class Diaspora return false; } - $item = Item::selectFirst(['id'], ['guid' => $parent_guid, 'origin' => true, 'private' => false]); + $item = Item::selectFirst(['id'], ['guid' => $parent_guid, 'origin' => true, 'private' => [Item::PUBLIC, Item::UNLISTED]]); if (!DBA::isResult($item)) { Logger::log('Item not found, no origin or private: '.$parent_guid); return false; @@ -2523,7 +2523,7 @@ class Diaspora // Do we already have this item? $fields = ['body', 'title', 'attach', 'tag', 'app', 'created', 'object-type', 'uri', 'guid', 'author-name', 'author-link', 'author-avatar']; - $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => false]; + $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => [Item::PUBLIC, Item::UNLISTED]]; $item = Item::selectFirst($fields, $condition); if (DBA::isResult($item)) { @@ -2567,7 +2567,7 @@ class Diaspora if ($stored) { $fields = ['body', 'title', 'attach', 'tag', 'app', 'created', 'object-type', 'uri', 'guid', 'author-name', 'author-link', 'author-avatar']; - $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => false]; + $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => [Item::PUBLIC, Item::UNLISTED]]; $item = Item::selectFirst($fields, $condition); if (DBA::isResult($item)) { @@ -2711,7 +2711,7 @@ class Diaspora $datarray["app"] = $original_item["app"]; $datarray["plink"] = self::plink($author, $guid); - $datarray["private"] = (($public == "false") ? 1 : 0); + $datarray["private"] = (($public == "false") ? Item::PRIVATE : Item::PUBLIC); $datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at; $datarray["object-type"] = $original_item["object-type"]; @@ -2941,7 +2941,7 @@ class Diaspora } $datarray["plink"] = self::plink($author, $guid); - $datarray["private"] = (($public == "false") ? 1 : 0); + $datarray["private"] = (($public == "false") ? Item::PRIVATE : Item::PUBLIC); $datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at; if (isset($address["address"])) { @@ -3245,7 +3245,7 @@ class Diaspora private static function sendParticipation(array $contact, array $item) { // Don't send notifications for private postings - if ($item['private']) { + if ($item['private'] == Item::PRIVATE) { return; } @@ -3536,12 +3536,12 @@ class Diaspora $myaddr = self::myHandle($owner); - $public = ($item["private"] ? "false" : "true"); + $public = ($item["private"] == Item::PRIVATE ? "false" : "true"); $created = DateTimeFormat::utc($item['received'], DateTimeFormat::ATOM); $edited = DateTimeFormat::utc($item["edited"] ?? $item["created"], DateTimeFormat::ATOM); // Detect a share element and do a reshare - if (!$item['private'] && ($ret = self::isReshare($item["body"]))) { + if (($item['private'] != Item::PRIVATE) && ($ret = self::isReshare($item["body"]))) { $message = ["author" => $myaddr, "guid" => $item["guid"], "created_at" => $created, @@ -4135,8 +4135,7 @@ class Diaspora $dob = DateTimeFormat::utc($year . '-' . $month . '-'. $day, 'Y-m-d'); } - $about = $profile['about']; - $about = strip_tags(BBCode::convert($about)); + $about = BBCode::toMarkdown($profile['about']); $location = Profile::formatLocation($profile); $tags = ''; diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index cbd50a097..4eb6c7294 100644 --- a/src/Protocol/Feed.php +++ b/src/Protocol/Feed.php @@ -220,7 +220,7 @@ class Feed { $header["wall"] = 0; $header["origin"] = 0; $header["gravity"] = GRAVITY_PARENT; - $header["private"] = 2; + $header["private"] = Item::PUBLIC; $header["verb"] = Activity::POST; $header["object-type"] = Activity\ObjectType::NOTE; diff --git a/src/Protocol/OStatus.php b/src/Protocol/OStatus.php index b5167aa72..96b8447b4 100644 --- a/src/Protocol/OStatus.php +++ b/src/Protocol/OStatus.php @@ -1682,7 +1682,7 @@ class OStatus $entry = self::entryHeader($doc, $owner, $item, $toplevel); - $condition = ['uid' => $owner["uid"], 'guid' => $repeated_guid, 'private' => false, + $condition = ['uid' => $owner["uid"], 'guid' => $repeated_guid, 'private' => [Item::PUBLIC, Item::UNLISTED], 'network' => [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]]; $repeated_item = Item::selectFirst([], $condition); if (!DBA::isResult($repeated_item)) { @@ -1827,7 +1827,7 @@ class OStatus { $item["id"] = $item["parent"] = 0; $item["created"] = $item["edited"] = date("c"); - $item["private"] = true; + $item["private"] = Item::PRIVATE; $contact = Probe::uri($item['follow']); @@ -2120,7 +2120,7 @@ class OStatus ]); } - if (!$item["private"] && !$feed_mode) { + if (($item['private'] != Item::PRIVATE) && !$feed_mode) { XML::addElement($doc, $entry, "link", "", ["rel" => "ostatus:attention", "href" => "http://activityschema.org/collection/public"]); XML::addElement($doc, $entry, "link", "", ["rel" => "mentioned", @@ -2212,8 +2212,8 @@ class OStatus $authorid = Contact::getIdForURL($owner["url"], 0, true); $condition = ["`uid` = ? AND `received` > ? AND NOT `deleted` - AND NOT `private` AND `visible` AND `wall` AND `parent-network` IN (?, ?)", - $owner["uid"], $check_date, Protocol::OSTATUS, Protocol::DFRN]; + AND `private` != ? AND `visible` AND `wall` AND `parent-network` IN (?, ?)", + $owner["uid"], $check_date, Item::PRIVATE, Protocol::OSTATUS, Protocol::DFRN]; if ($filter === 'comments') { $condition[0] .= " AND `object-type` = ? "; diff --git a/src/Repository/Notify.php b/src/Repository/Notify.php index d8887affd..b72ccecf0 100644 --- a/src/Repository/Notify.php +++ b/src/Repository/Notify.php @@ -23,9 +23,9 @@ namespace Friendica\Repository; use Exception; use Friendica\BaseRepository; +use Friendica\Collection; use Friendica\Core\Hook; use Friendica\Model; -use Friendica\Collection; use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Util\DateTimeFormat; @@ -61,14 +61,17 @@ class Notify extends BaseRepository } /** - * {@inheritDoc} + * Return one notify instance based on ID / UID + * + * @param int $id The ID of the notify instance + * @param int $uid The user ID, bound to this notify instance (= security check) * * @return Model\Notify * @throws NotFoundException */ - public function getByID(int $id) + public function getByID(int $id, int $uid) { - return $this->selectFirst(['id' => $id, 'uid' => local_user()]); + return $this->selectFirst(['id' => $id, 'uid' => $uid]); } /** diff --git a/src/Util/JsonLD.php b/src/Util/JsonLD.php index 1452318b5..b4ff53fdb 100644 --- a/src/Util/JsonLD.php +++ b/src/Util/JsonLD.php @@ -122,7 +122,9 @@ class JsonLD 'ostatus' => (object)['@id' => 'http://ostatus.org#', '@type' => '@id'], 'dc' => (object)['@id' => 'http://purl.org/dc/terms/', '@type' => '@id'], 'toot' => (object)['@id' => 'http://joinmastodon.org/ns#', '@type' => '@id'], - 'litepub' => (object)['@id' => 'http://litepub.social/ns#', '@type' => '@id']]; + 'litepub' => (object)['@id' => 'http://litepub.social/ns#', '@type' => '@id'], + 'sc' => (object)['@id' => 'http://schema.org#', '@type' => '@id'], + 'pt' => (object)['@id' => 'https://joinpeertube.org/ns#', '@type' => '@id']]; // Preparation for adding possibly missing content to the context if (!empty($json['@context']) && is_string($json['@context'])) { diff --git a/src/Worker/DBClean.php b/src/Worker/DBClean.php index 4144fc95e..7fbc65e29 100644 --- a/src/Worker/DBClean.php +++ b/src/Worker/DBClean.php @@ -107,6 +107,7 @@ class DBClean { Logger::log("found global item orphans: ".$count); while ($orphan = DBA::fetch($r)) { $last_id = $orphan["id"]; + Logger::notice('Delete global orphan item', ['id' => $orphan["id"]]); DBA::delete('item', ['id' => $orphan["id"]]); } Worker::add(PRIORITY_MEDIUM, 'DBClean', 1, $last_id); @@ -129,6 +130,7 @@ class DBClean { Logger::log("found item orphans without parents: ".$count); while ($orphan = DBA::fetch($r)) { $last_id = $orphan["id"]; + Logger::notice('Delete orphan item', ['id' => $orphan["id"]]); DBA::delete('item', ['id' => $orphan["id"]]); } Worker::add(PRIORITY_MEDIUM, 'DBClean', 2, $last_id); @@ -326,6 +328,7 @@ class DBClean { Logger::log("found global item entries from expired threads: ".$count); while ($orphan = DBA::fetch($r)) { $last_id = $orphan["id"]; + Logger::notice('Delete expired thread item', ['id' => $orphan["id"]]); DBA::delete('item', ['id' => $orphan["id"]]); } Worker::add(PRIORITY_MEDIUM, 'DBClean', 9, $last_id); diff --git a/src/Worker/Delivery.php b/src/Worker/Delivery.php index c69545bbd..01f747644 100644 --- a/src/Worker/Delivery.php +++ b/src/Worker/Delivery.php @@ -176,7 +176,7 @@ class Delivery && empty($parent['allow_gid']) && empty($parent['deny_cid']) && empty($parent['deny_gid']) - && !$parent["private"]) { + && ($parent["private"] != Model\Item::PRIVATE)) { $public_message = true; } } diff --git a/src/Worker/Expire.php b/src/Worker/Expire.php index e1e671532..dfbf9738e 100644 --- a/src/Worker/Expire.php +++ b/src/Worker/Expire.php @@ -45,6 +45,7 @@ class Expire $condition = ["`deleted` AND `changed` < UTC_TIMESTAMP() - INTERVAL 60 DAY"]; $rows = DBA::select('item', ['id'], $condition); while ($row = DBA::fetch($rows)) { + Logger::notice('Delete expired item', ['id' => $row["id"]]); DBA::delete('item', ['id' => $row['id']]); } DBA::close($rows); diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index b3741e546..35a228fce 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -151,6 +151,8 @@ class Notifier // If this is a public conversation, notify the feed hub $public_message = true; + $unlisted = false; + // Do a PuSH $push_notify = false; @@ -183,6 +185,8 @@ class Notifier Logger::info('Threaded comment', ['diaspora_delivery' => (int)$diaspora_delivery]); } + $unlisted = $target_item['private'] == Item::UNLISTED; + // This is IMPORTANT!!!! // We will only send a "notify owner to relay" or followup message if the referenced post @@ -245,8 +249,7 @@ class Notifier Logger::info('Followup', ['target' => $target_id, 'guid' => $target_item['guid'], 'to' => $parent['contact-id']]); - //if (!$target_item['private'] && $target_item['wall'] && - if (!$target_item['private'] && + if (($target_item['private'] != Item::PRIVATE) && (strlen($target_item['allow_cid'].$target_item['allow_gid']. $target_item['deny_cid'].$target_item['deny_gid']) == 0)) $push_notify = true; @@ -410,7 +413,7 @@ class Notifier if ($public_message && !in_array($cmd, [Delivery::MAIL, Delivery::SUGGESTION]) && !$followup) { $relay_list = []; - if ($diaspora_delivery) { + if ($diaspora_delivery && !$unlisted) { $batch_delivery = true; $relay_list_stmt = DBA::p( diff --git a/src/Worker/OnePoll.php b/src/Worker/OnePoll.php index c87bfcf25..959d28237 100644 --- a/src/Worker/OnePoll.php +++ b/src/Worker/OnePoll.php @@ -657,11 +657,11 @@ class OnePoll $datarray['owner-avatar'] = $contact['photo']; if ($datarray['parent-uri'] === $datarray['uri']) { - $datarray['private'] = 1; + $datarray['private'] = Item::PRIVATE; } if (!DI::pConfig()->get($importer_uid, 'system', 'allow_public_email_replies')) { - $datarray['private'] = 1; + $datarray['private'] = Item::PRIVATE; $datarray['allow_cid'] = '<' . $contact['id'] . '>'; } diff --git a/src/Worker/RemoveContact.php b/src/Worker/RemoveContact.php index ba464f75f..40e3a67fc 100644 --- a/src/Worker/RemoveContact.php +++ b/src/Worker/RemoveContact.php @@ -21,6 +21,7 @@ namespace Friendica\Worker; +use Friendica\Core\Logger; use Friendica\Database\DBA; use Friendica\Core\Protocol; use Friendica\Model\Item; @@ -42,6 +43,7 @@ class RemoveContact { do { $items = Item::select(['id'], $condition, ['limit' => 100]); while ($item = Item::fetch($items)) { + Logger::notice('Delete removed contact item', ['id' => $item["id"]]); DBA::delete('item', ['id' => $item['id']]); } DBA::close($items); diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 978666460..5b2e3bc09 100755 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -51,7 +51,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1333); + define('DB_UPDATE_VERSION', 1334); } return [ @@ -647,7 +647,7 @@ return [ "extid" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], "post-type" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "Post type (personal note, bookmark, ...)"], "global" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], - "private" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "distribution is restricted"], + "private" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "0=public, 1=private, 2=unlisted"], "visible" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "moderated" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "deleted" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "item has been deleted"], @@ -1294,7 +1294,7 @@ return [ "received" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], "changed" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""], "wall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], - "private" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], + "private" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "0=public, 1=private, 2=unlisted"], "pubmail" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "moderated" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "visible" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], diff --git a/tests/datasets/content/text/html/bug-7474.html b/tests/datasets/content/text/html/bug-7474.html index 0bba94e63..1ed97bce6 100644 --- a/tests/datasets/content/text/html/bug-7474.html +++ b/tests/datasets/content/text/html/bug-7474.html @@ -1 +1 @@ -
I recently released a PHP package that makes executing commands over SSH super simple. You can also upload/download files via SCP.
https://github.com/DivineOmega/php-ssh-connection
#php #opensource #webdev #ssh #DevOps
\ No newline at end of file +I recently released a PHP package that makes executing commands over SSH super simple. You can also upload/download files via SCP.
https://github.com/DivineOmega/php-ssh-connection
#php #opensource #webdev #ssh #DevOps
\ No newline at end of file diff --git a/tests/src/Content/Text/BBCodeTest.php b/tests/src/Content/Text/BBCodeTest.php index f827eb5b1..1a1d06dc7 100644 --- a/tests/src/Content/Text/BBCodeTest.php +++ b/tests/src/Content/Text/BBCodeTest.php @@ -164,7 +164,7 @@ class BBCodeTest extends MockedTest public function testAutoLinking($data, $assertHTML) { $output = BBCode::convert($data); - $assert = '' . $data . ''; + $assert = '' . $data . ''; if ($assertHTML) { $this->assertEquals($assert, $output); } else { @@ -176,21 +176,21 @@ class BBCodeTest extends MockedTest { return [ 'bug-7271-condensed-space' => [ - 'expectedHtml' => '', + 'expectedHtml' => '', 'text' => '[ol][*] http://example.com/[/ol]', ], 'bug-7271-condensed-nospace' => [ - 'expectedHtml' => '', + 'expectedHtml' => '', 'text' => '[ol][*]http://example.com/[/ol]', ], 'bug-7271-indented-space' => [ - 'expectedHtml' => '', + 'expectedHtml' => '', 'text' => '[ul] [*] http://example.com/ [/ul]', ], 'bug-7271-indented-nospace' => [ - 'expectedHtml' => '', + 'expectedHtml' => '', 'text' => '[ul] [*]http://example.com/ [/ul]', diff --git a/view/templates/admin/users.tpl b/view/templates/admin/users.tpl index 6ca85fe7a..8bbffbb7d 100644 --- a/view/templates/admin/users.tpl +++ b/view/templates/admin/users.tpl @@ -35,8 +35,8 @@