Merge branch 'friendica:develop' into 6606-k-alin-mysql-unix-socket

This commit is contained in:
k-alin 2022-02-20 17:27:03 +01:00 committed by GitHub
commit 50580fd04c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1429 additions and 1093 deletions

View file

@ -1,5 +1,6 @@
Version 2022.05 (unreleased)
Friendica Core
Breaking: The distribution of _private forums_ was moved to ActivityPub, making them incompatible with older versions of Friendica [annando]
Friendica Addons

View file

@ -51,7 +51,7 @@
"pragmarx/recovery": "^0.2",
"psr/container": "^1.0",
"seld/cli-prompt": "^1.0",
"smarty/smarty": "^3.1",
"smarty/smarty": "^4",
"ua-parser/uap-php": "^3.9",
"xemlock/htmlpurifier-html5": "^0.1.11",
"fxp/composer-asset-plugin": "^1.4",

108
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3d221e30c9cb7e3f34d8d8141b6fea6c",
"content-hash": "f5922f03b367e68a5930df6ed80c5c2f",
"packages": [
{
"name": "asika/simple-console",
@ -1152,6 +1152,24 @@
"html",
"markdown"
],
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
},
{
"url": "https://www.patreon.com/colinodell",
"type": "patreon"
}
],
"time": "2020-07-01T00:34:03+00:00"
},
{
@ -1481,6 +1499,12 @@
"mobile detector",
"php mobile detect"
],
"funding": [
{
"url": "https://github.com/serbanghita",
"type": "github"
}
],
"time": "2021-02-19T21:22:57+00:00"
},
{
@ -1553,6 +1577,16 @@
"logging",
"psr-3"
],
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
"type": "tidelift"
}
],
"time": "2021-05-28T08:32:12+00:00"
},
{
@ -3647,29 +3681,29 @@
},
{
"name": "smarty/smarty",
"version": "v3.1.43",
"version": "v4.1.0",
"source": {
"type": "git",
"url": "https://github.com/smarty-php/smarty.git",
"reference": "273f7e00fec034f6d61112552e9caf08d19565b7"
"reference": "9e0536de18b53ba193364291ef0303b0ab9903e1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/smarty-php/smarty/zipball/273f7e00fec034f6d61112552e9caf08d19565b7",
"reference": "273f7e00fec034f6d61112552e9caf08d19565b7",
"url": "https://api.github.com/repos/smarty-php/smarty/zipball/9e0536de18b53ba193364291ef0303b0ab9903e1",
"reference": "9e0536de18b53ba193364291ef0303b0ab9903e1",
"shasum": ""
},
"require": {
"php": ">=5.2"
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^6.5 || ^5.7 || ^4.8",
"phpunit/phpunit": "^8.5 || ^7.5",
"smarty/smarty-lexer": "^3.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1.x-dev"
"dev-master": "4.0.x-dev"
}
},
"autoload": {
@ -3693,14 +3727,18 @@
{
"name": "Rodney Rehm",
"email": "rodney.rehm@medialize.de"
},
{
"name": "Simon Wisselink",
"homepage": "https://www.iwink.nl/"
}
],
"description": "Smarty - the compiling PHP template engine",
"homepage": "http://www.smarty.net",
"homepage": "https://smarty-php.github.io/smarty/",
"keywords": [
"templating"
],
"time": "2022-01-10T09:52:40+00:00"
"time": "2022-02-06T20:34:27+00:00"
},
{
"name": "spomky-labs/base64url",
@ -3751,6 +3789,16 @@
"safe",
"url"
],
"funding": [
{
"url": "https://github.com/Spomky",
"type": "github"
},
{
"url": "https://www.patreon.com/FlorentMorselli",
"type": "patreon"
}
],
"time": "2020-11-03T09:10:25+00:00"
},
{
@ -4613,6 +4661,20 @@
"constructor",
"instantiate"
],
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
"type": "tidelift"
}
],
"time": "2020-11-10T18:47:58+00:00"
},
{
@ -4822,6 +4884,12 @@
"object",
"object graph"
],
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2020-11-13T09:40:50+00:00"
},
{
@ -6547,6 +6615,20 @@
"polyfill",
"portable"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
},
{
@ -6587,6 +6669,12 @@
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"funding": [
{
"url": "https://github.com/theseer",
"type": "github"
}
],
"time": "2021-07-28T10:34:58+00:00"
},
{

View file

@ -1,6 +1,6 @@
-- ------------------------------------------
-- Friendica 2022.05-dev (Siberian Iris)
-- DB_UPDATE_VERSION 1451
-- DB_UPDATE_VERSION 1452
-- ------------------------------------------
@ -1277,7 +1277,7 @@ CREATE TABLE IF NOT EXISTS `post-thread-user` (
`wall` boolean NOT NULL DEFAULT '0' COMMENT 'This item was posted to the wall of uid',
`mention` boolean NOT NULL DEFAULT '0' COMMENT '',
`pubmail` boolean NOT NULL DEFAULT '0' COMMENT '',
`forum_mode` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '',
`forum_mode` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Deprecated',
`contact-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'contact.id',
`unseen` boolean NOT NULL DEFAULT '1' COMMENT 'post has not been seen',
`hidden` boolean NOT NULL DEFAULT '0' COMMENT 'Marker to hide the post from the user',
@ -1612,7 +1612,6 @@ CREATE VIEW `post-user-view` AS SELECT
`post-user`.`deleted` AS `deleted`,
`post-user`.`origin` AS `origin`,
`post-thread-user`.`origin` AS `parent-origin`,
`post-thread-user`.`forum_mode` AS `forum_mode`,
`post-thread-user`.`mention` AS `mention`,
`post-user`.`global` AS `global`,
`post-user`.`network` AS `network`,
@ -1773,7 +1772,6 @@ CREATE VIEW `post-thread-user-view` AS SELECT
`post-thread-user`.`unseen` AS `unseen`,
`post-user`.`deleted` AS `deleted`,
`post-thread-user`.`origin` AS `origin`,
`post-thread-user`.`forum_mode` AS `forum_mode`,
`post-thread-user`.`mention` AS `mention`,
`post-user`.`global` AS `global`,
`post-thread-user`.`network` AS `network`,

View file

@ -24,7 +24,7 @@ Fields
| wall | This item was posted to the wall of uid | boolean | NO | | 0 | |
| mention | | boolean | NO | | 0 | |
| pubmail | | boolean | NO | | 0 | |
| forum_mode | | tinyint unsigned | NO | | 0 | |
| forum_mode | Deprecated | tinyint unsigned | NO | | 0 | |
| contact-id | contact.id | int unsigned | NO | | 0 | |
| unseen | post has not been seen | boolean | NO | | 1 | |
| hidden | Marker to hide the post from the user | boolean | NO | | 0 | |

View file

@ -23,7 +23,6 @@ use Friendica\App;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Widget;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Core\Session;
use Friendica\Database\DBA;
@ -36,6 +35,7 @@ use Friendica\Module\ActivityPub\Objects;
use Friendica\Network\HTTPException;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\DFRN;
use Friendica\Protocol\Diaspora;
function display_init(App $a)
{
@ -108,55 +108,23 @@ function display_init(App $a)
$item = $parent ?: $item;
}
$profiledata = display_fetchauthor($item);
DI::page()['aside'] = Widget\VCard::getHTML($profiledata);
DI::page()['aside'] = Widget\VCard::getHTML(display_fetchauthor($item));
}
function display_fetchauthor($item)
{
$profiledata = Contact::getByURLForUser($item['author-link'], local_user());
// Check for a repeated message
$shared = Item::getShareArray($item);
if (!empty($shared) && empty($shared['comment'])) {
$profiledata = [
'uid' => 0,
'id' => -1,
'nickname' => '',
'name' => '',
'picdate' => '',
'photo' => '',
'url' => '',
'network' => '',
];
if (!empty($shared['author'])) {
$profiledata['name'] = $shared['author'];
}
if (Diaspora::isReshare($item['body'], true)) {
$shared = Item::getShareArray($item);
if (!empty($shared['profile'])) {
$profiledata['url'] = $shared['profile'];
$contact = Contact::getByURLForUser($shared['profile'], local_user());
}
if (!empty($shared['avatar'])) {
$profiledata['photo'] = $shared['avatar'];
}
$profiledata['nickname'] = $profiledata['name'];
$profiledata['network'] = Protocol::PHANTOM;
$profiledata['address'] = '';
$profiledata['about'] = '';
$profiledata = Contact::getByURLForUser($profiledata['url'], local_user()) ?: $profiledata;
}
if (!empty($profiledata['photo'])) {
$profiledata['photo'] = DI::baseUrl()->remove($profiledata['photo']);
if (empty($contact)) {
$contact = Contact::getById($item['author-id']);
}
return $profiledata;
return $contact;
}
function display_content(App $a, $update = false, $update_uid = 0)

View file

@ -29,7 +29,6 @@
*/
use Friendica\App;
use Friendica\Content\Item as ItemHelper;
use Friendica\Content\PageInfo;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Hook;
@ -40,11 +39,11 @@ use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\APContact;
use Friendica\Model\Attach;
use Friendica\Model\Contact;
use Friendica\Model\Conversation;
use Friendica\Model\FileTag;
use Friendica\Model\Group;
use Friendica\Model\Item;
use Friendica\Model\ItemURI;
use Friendica\Model\Notification;
@ -404,7 +403,7 @@ function item_post(App $a) {
}
$inform .= 'cid:' . $contact['id'];
if (!$toplevel_item_id || empty($contact['cid']) || ($contact['contact-type'] != Contact::TYPE_COMMUNITY)) {
if ($toplevel_item_id || empty($contact['cid']) || ($contact['contact-type'] != Contact::TYPE_COMMUNITY)) {
continue;
}
@ -437,26 +436,15 @@ function item_post(App $a) {
$postopts = '';
}
if (!$private_forum) {
$str_contact_deny = '';
$str_group_deny = '';
if ($private_forum) {
$str_contact_allow = '<' . $private_id . '>';
$str_group_allow = '<' . Group::getIdForForum($forum_contact['id']) . '>';
} else {
$str_contact_allow = '';
$str_group_allow = '';
$str_contact_deny = '';
$str_group_deny = '';
}
if ($private_forum || !APContact::getByURL($forum_contact['url'])) {
$str_group_allow = '';
$str_contact_deny = '';
$str_group_deny = '';
if ($private_forum) {
$str_contact_allow = '<' . $private_id . '>';
} else {
$str_contact_allow = '';
}
$contact_id = $private_id;
$contact_record = $forum_contact;
$_REQUEST['origin'] = false;
$wall = 0;
}
}

View file

@ -31,6 +31,7 @@ use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Group;
use Friendica\Model\Item;
use Friendica\Model\Notification;
use Friendica\Model\Profile;
use Friendica\Model\User;
@ -239,7 +240,6 @@ function settings_post(App $a)
$allow_location = ((!empty($_POST['allow_location']) && (intval($_POST['allow_location']) == 1)) ? 1: 0);
$publish = ((!empty($_POST['profile_in_directory']) && (intval($_POST['profile_in_directory']) == 1)) ? 1: 0);
$net_publish = ((!empty($_POST['profile_in_netdirectory']) && (intval($_POST['profile_in_netdirectory']) == 1)) ? 1: 0);
$old_visibility = ((!empty($_POST['visibility']) && (intval($_POST['visibility']) == 1)) ? 1 : 0);
$account_type = ((!empty($_POST['account-type']) && (intval($_POST['account-type']))) ? intval($_POST['account-type']) : 0);
$page_flags = ((!empty($_POST['page-flags']) && (intval($_POST['page-flags']))) ? intval($_POST['page-flags']) : 0);
$blockwall = ((!empty($_POST['blockwall']) && (intval($_POST['blockwall']) == 1)) ? 0: 1); // this setting is inverted!
@ -361,16 +361,21 @@ function settings_post(App $a)
DI::pConfig()->set(local_user(), 'system', 'unlisted', $unlisted);
DI::pConfig()->set(local_user(), 'system', 'accessible-photos', $accessiblephotos);
if ($account_type == User::ACCOUNT_TYPE_COMMUNITY) {
$str_group_allow = '';
$str_contact_allow = '';
$str_group_deny = '';
$str_contact_deny = '';
DI::pConfig()->set(local_user(), 'system', 'unlisted', true);
$blockwall = true;
$blocktags = true;
$hide_friends = true;
}
if ($page_flags == User::PAGE_FLAGS_PRVGROUP) {
$hidewall = 1;
if (!$str_contact_allow && !$str_group_allow && !$str_contact_deny && !$str_group_deny) {
if ($def_gid) {
info(DI::l10n()->t('Private forum has no privacy permissions. Using default privacy group.'));
$str_group_allow = '<' . $def_gid . '>';
} else {
notice(DI::l10n()->t('Private forum has no privacy permissions and no default privacy group.'));
}
}
$str_group_allow = '<' . Group::FOLLOWERS . '>';
}
$fields = ['username' => $username, 'email' => $email, 'timezone' => $timezone,
@ -570,7 +575,17 @@ function settings_content(App $a)
'$ostat_enabled' => $ostat_enabled,
'$general_settings' => DI::l10n()->t('General Social Media Settings'),
'$accept_only_sharer' => ['accept_only_sharer', DI::l10n()->t('Accept only top level posts by contacts you follow'), $accept_only_sharer, DI::l10n()->t('The system does an auto completion of threads when a comment arrives. This has got the side effect that you can receive posts that had been started by a non-follower but had been commented by someone you follow. This setting deactivates this behaviour. When activated, you strictly only will receive posts from people you really do follow.')],
'$accept_only_sharer' => [
'accept_only_sharer',
DI::l10n()->t('Followed content scope'),
$accept_only_sharer,
DI::l10n()->t('By default, conversations in which your follows participated but didn\'t start will be shown in your timeline. You can turn this behavior off, or expand it to the conversations in which your follows liked a post.'),
[
Item::COMPLETION_NONE => DI::l10n()->t('Only conversations my follows started'),
Item::COMPLETION_COMMENT => DI::l10n()->t('Conversations my follows started or commented on (default)'),
Item::COMPLETION_LIKE => DI::l10n()->t('Any conversation my follows interacted with, including likes'),
]
],
'$enable_cw' => ['enable_cw', DI::l10n()->t('Enable Content Warning'), $enable_cw, DI::l10n()->t('Users on networks like Mastodon or Pleroma are able to set a content warning field which collapse their post by default. This enables the automatic collapsing instead of setting the content warning as the post title. Doesn\'t affect any other content filtering you eventually set up.')],
'$enable_smart_shortening' => ['enable_smart_shortening', DI::l10n()->t('Enable intelligent shortening'), $enable_smart_shortening, DI::l10n()->t('Normally the system tries to find the best link to add to shortened posts. If disabled, every shortened post will always point to the original friendica post.')],
'$simple_shortening' => ['simple_shortening', DI::l10n()->t('Enable simple text shortening'), $simple_shortening, DI::l10n()->t('Normally the system shortens posts at the next line feed. If this option is enabled then the system will shorten the text at the maximum character limit.')],
@ -756,7 +771,7 @@ function settings_content(App $a)
'$allowloc' => ['allow_location', DI::l10n()->t('Use Browser Location:'), ($user['allow_location'] == 1), ''],
'$h_prv' => DI::l10n()->t('Security and Privacy Settings'),
'$visibility' => $profile['net-publish'],
'$is_community' => ($user['account-type'] == User::ACCOUNT_TYPE_COMMUNITY),
'$maxreq' => ['maxreq', DI::l10n()->t('Maximum Friend Requests/Day:'), $maxreq , DI::l10n()->t("\x28to prevent spam abuse\x29")],
'$profile_in_dir' => $profile_in_dir,
'$profile_in_net_dir' => ['profile_in_netdirectory', DI::l10n()->t('Allow your profile to be searchable globally?'), $profile['net-publish'], DI::l10n()->t("Activate this setting if you want others to easily find and follow you. Your profile will be searchable on remote systems. This setting also determines whether Friendica will inform search engines that your profile should be indexed or not.") . $net_pub_desc],

View file

@ -151,7 +151,7 @@ HELP;
$this->out("{$cat}.{$key}[{$k}] => " . (is_array($v) ? implode(', ', $v) : $v));
}
} else {
$this->out("{$cat}.{$key} => " . $value);
$this->out("{$cat}.{$key} => " . ($value ?? 'NULL'));
}
}

View file

@ -318,23 +318,20 @@ class Widget
/**
* Return categories widget
*
* @param string $baseurl baseurl
* @param string $selected optional, default empty
* @param int $uid Id of the user owning the categories
* @param string $baseurl Base page URL
* @param string $selected Selected category
* @return string|void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function categories($baseurl, $selected = '')
public static function categories(int $uid, string $baseurl, string $selected = '')
{
$a = DI::app();
$uid = intval($a->getProfileOwner());
if (!Feature::isEnabled($uid, 'categories')) {
return '';
}
$terms = array();
foreach (Post\Category::getArray(local_user(), Post\Category::CATEGORY) as $savedFolderName) {
foreach (Post\Category::getArray($uid, Post\Category::CATEGORY) as $savedFolderName) {
$terms[] = ['ref' => $savedFolderName, 'name' => $savedFolderName];
}

View file

@ -59,13 +59,13 @@ class RedisCache extends AbstractCache implements ICanCacheInMemory
$redis_pw = $config->get('system', 'redis_password');
$redis_db = $config->get('system', 'redis_db', 0);
if (isset($redis_port) && !@$this->redis->connect($redis_host, $redis_port)) {
if (!empty($redis_port) && !@$this->redis->connect($redis_host, $redis_port)) {
throw new CachePersistenceException('Expected Redis server at ' . $redis_host . ':' . $redis_port . ' isn\'t available');
} elseif (!@$this->redis->connect($redis_host)) {
throw new CachePersistenceException('Expected Redis server at ' . $redis_host . ' isn\'t available');
}
if (isset($redis_pw) && !$this->redis->auth($redis_pw)) {
if (!empty($redis_pw) && !$this->redis->auth($redis_pw)) {
throw new CachePersistenceException('Cannot authenticate redis server at ' . $redis_host . ':' . $redis_port);
}

View file

@ -25,6 +25,7 @@ use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Conversation;
use Friendica\Model\GServer;
use Friendica\Model\Item;
use Friendica\Model\ItemURI;
@ -33,6 +34,9 @@ use Friendica\Model\Post;
use Friendica\Model\Post\Category;
use Friendica\Model\Tag;
use Friendica\Model\Verb;
use Friendica\Protocol\ActivityPub\Processor;
use Friendica\Protocol\ActivityPub\Receiver;
use Friendica\Util\JsonLD;
use Friendica\Util\Strings;
/**
@ -46,7 +50,7 @@ class PostUpdate
// Needed for the helper function to read from the legacy term table
const OBJECT_TYPE_POST = 1;
const VERSION = 1427;
const VERSION = 1452;
/**
* Calls the post update functions
@ -104,6 +108,9 @@ class PostUpdate
if (!self::update1427()) {
return false;
}
if (!self::update1452()) {
return false;
}
return true;
}
@ -1012,4 +1019,70 @@ class PostUpdate
return false;
}
/**
* Fill the receivers of the post via the raw source
*
* @return bool "true" when the job is done
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function update1452()
{
// Was the script completed?
if (DI::config()->get('system', 'post_update_version') >= 1452) {
return true;
}
$id = DI::config()->get('system', 'post_update_version_1452_id', 0);
Logger::info('Start', ['uri-id' => $id]);
$start_id = $id;
$rows = 0;
$conversations = DBA::p("SELECT `post-view`.`uri-id`, `conversation`.`source`, `conversation`.`received` FROM `conversation`
INNER JOIN `post-view` ON `post-view`.`uri` = `conversation`.`item-uri`
WHERE NOT `source` IS NULL AND `conversation`.`protocol` = ? AND `uri-id` > ? LIMIT ?",
Conversation::PARCEL_ACTIVITYPUB, $id, 1000);
if (DBA::errorNo() != 0) {
Logger::error('Database error', ['no' => DBA::errorNo(), 'message' => DBA::errorMessage()]);
return false;
}
while ($conversation = DBA::fetch($conversations)) {
$id = $conversation['uri-id'];
$received = $conversation['received'];
$raw = json_decode($conversation['source'], true);
if (empty($raw)) {
continue;
}
$activity = JsonLD::compact($raw);
$urls = Receiver::getReceiverURL($activity);
Processor::storeReceivers($conversation['uri-id'], $urls);
if (!empty($activity['as:object'])) {
$urls = array_merge($urls, Receiver::getReceiverURL($activity['as:object']));
Processor::storeReceivers($conversation['uri-id'], $urls);
}
++$rows;
}
DBA::close($conversations);
DI::config()->set('system', 'post_update_version_1452_id', $id);
Logger::info('Processed', ['rows' => $rows, 'last' => $id, 'last-received' => $received]);
if ($start_id == $id) {
DI::config()->set('system', 'post_update_version', 1452);
Logger::info('Done');
return true;
}
return false;
}
}

View file

@ -685,7 +685,7 @@ class Contact
*/
public static function updateSelfFromUserID($uid, $update_avatar = false)
{
$fields = ['id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar', 'prvkey', 'pubkey',
$fields = ['id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar', 'prvkey', 'pubkey', 'manually-approve',
'xmpp', 'matrix', 'contact-type', 'forum', 'prv', 'avatar-date', 'url', 'nurl', 'unsearchable',
'photo', 'thumb', 'micro', 'header', 'addr', 'request', 'notify', 'poll', 'confirm', 'poco', 'network'];
$self = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]);
@ -757,6 +757,7 @@ class Contact
$fields['forum'] = $user['page-flags'] == User::PAGE_FLAGS_COMMUNITY;
$fields['prv'] = $user['page-flags'] == User::PAGE_FLAGS_PRVGROUP;
$fields['unsearchable'] = !$profile['net-publish'];
$fields['manually-approve'] = in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]);
$update = false;

View file

@ -41,7 +41,7 @@ class Group
public static function getByUserId($uid, $includesDeleted = false)
{
$conditions = ['uid' => $uid];
$conditions = ['uid' => $uid, 'cid' => null];
if (!$includesDeleted) {
$conditions['deleted'] = false;
@ -310,6 +310,68 @@ class Group
return DBA::delete('group_member', ['gid' => $gid, 'contact-id' => $cid]);
}
/**
* Adds contacts to a group
*
* @param int $gid
* @param array $contacts
* @throws \Exception
*/
public static function addMembers(int $gid, array $contacts)
{
if (!$gid || !$contacts) {
return false;
}
// @TODO Backward compatibility with user contacts, remove by version 2022.03
$group = DBA::selectFirst('group', ['uid'], ['id' => $gid]);
if (empty($group)) {
throw new HTTPException\NotFoundException('Group not found.');
}
foreach ($contacts as $cid) {
$cdata = Contact::getPublicAndUserContactID($cid, $group['uid']);
if (empty($cdata['user'])) {
throw new HTTPException\NotFoundException('Invalid contact.');
}
DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $cdata['user']], Database::INSERT_IGNORE);
}
}
/**
* Removes contacts from a group
*
* @param int $gid
* @param array $contacts
* @throws \Exception
*/
public static function removeMembers(int $gid, array $contacts)
{
if (!$gid || !$contacts) {
return false;
}
// @TODO Backward compatibility with user contacts, remove by version 2022.03
$group = DBA::selectFirst('group', ['uid'], ['id' => $gid]);
if (empty($group)) {
throw new HTTPException\NotFoundException('Group not found.');
}
$contactIds = [];
foreach ($contacts as $cid) {
$cdata = Contact::getPublicAndUserContactID($cid, $group['uid']);
if (empty($cdata['user'])) {
throw new HTTPException\NotFoundException('Invalid contact.');
}
$contactIds[] = $cdata['user'];
}
DBA::delete('group_member', ['gid' => $gid, 'contact-id' => $contactIds]);
}
/**
* Returns the combined list of contact ids from a group id list
*
@ -408,7 +470,7 @@ class Group
]
];
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => $uid], ['order' => ['name']]);
$stmt = DBA::select('group', [], ['deleted' => false, 'uid' => $uid, 'cid' => null], ['order' => ['name']]);
while ($group = DBA::fetch($stmt)) {
$display_groups[] = [
'name' => $group['name'],
@ -465,7 +527,7 @@ class Group
$member_of = self::getIdsByContactId($cid);
}
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => local_user()], ['order' => ['name']]);
$stmt = DBA::select('group', [], ['deleted' => false, 'uid' => local_user(), 'cid' => null], ['order' => ['name']]);
while ($group = DBA::fetch($stmt)) {
$selected = (($group_id == $group['id']) ? ' group-selected' : '');
@ -522,21 +584,19 @@ class Group
}
/**
* Fetch the followers of a given contact id and store them as group members
* Fetch the group id for the given contact id
*
* @param integer $id Contact ID
* @return integer Group IO
*/
public static function getMembersForForum(int $id) {
$contact = Contact::getById($id, ['uid', 'url', 'name']);
if (empty($contact)) {
return;
public static function getIdForForum(int $id)
{
Logger::info('Get id for forum id', ['id' => $id]);
$contact = Contact::getById($id, ['uid', 'name', 'contact-type', 'manually-approve']);
if (empty($contact) || ($contact['contact-type'] != Contact::TYPE_COMMUNITY) || !$contact['manually-approve']) {
return 0;
}
$apcontact = APContact::getByURL($contact['url']);
if (empty($apcontact['followers'])) {
return;
}
$group = DBA::selectFirst('group', ['id'], ['uid' => $contact['uid'], 'cid' => $id]);
if (empty($group)) {
$fields = [
@ -549,15 +609,42 @@ class Group
} else {
$gid = $group['id'];
}
return $gid;
}
/**
* Fetch the followers of a given contact id and store them as group members
*
* @param integer $id Contact ID
*/
public static function updateMembersForForum(int $id)
{
Logger::info('Update forum members', ['id' => $id]);
$contact = Contact::getById($id, ['uid', 'url']);
if (empty($contact)) {
return;
}
$apcontact = APContact::getByURL($contact['url']);
if (empty($apcontact['followers'])) {
return;
}
$gid = self::getIdForForum($id);
if (empty($gid)) {
return;
}
$group_members = DBA::selectToArray('group_member', ['contact-id'], ['gid' => $gid]);
if (!empty($group_members)) {
$current = array_unique(array_column($group_members, 'contact-id'));
} else {
$current = [];
}
foreach (ActivityPub::fetchItems($apcontact['followers']) as $follower) {
foreach (ActivityPub::fetchItems($apcontact['followers'], $contact['uid']) as $follower) {
$id = Contact::getIdForURL($follower);
if (!in_array($id, $current)) {
DBA::insert('group_member', ['gid' => $gid, 'contact-id' => $id]);
@ -566,7 +653,8 @@ class Group
unset($current[$key]);
}
}
DBA::delete('group_member', ['gid' => $gid, 'contact-id' => $current]);
Logger::info('Updated forum members', ['id' => $id, 'count' => DBA::count('group_member', ['gid' => $gid])]);
}
}

View file

@ -74,6 +74,11 @@ class Item
const PR_RELAY = 74;
const PR_FETCHED = 75;
// system.accept_only_sharer setting values
const COMPLETION_NONE = 1;
const COMPLETION_COMMENT = 0;
const COMPLETION_LIKE = 2;
// Field list that is used to display the items
const DISPLAY_FIELDLIST = [
'uid', 'id', 'parent', 'guid', 'network', 'gravity',
@ -100,7 +105,7 @@ class Item
'inform', 'deleted', 'extid', 'post-type', 'post-reason', 'gravity',
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
'author-id', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'contact-uid',
'signed_text', 'network', 'wall', 'contact-id', 'plink', 'forum_mode', 'origin',
'signed_text', 'network', 'wall', 'contact-id', 'plink', 'origin',
'thr-parent-id', 'parent-uri-id', 'postopts', 'pubmail',
'event-created', 'event-edited', 'event-start', 'event-finish',
'event-summary', 'event-desc', 'event-location', 'event-type',
@ -114,7 +119,7 @@ class Item
'postopts', 'plink', 'resource-id', 'event-id', 'inform',
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'post-type', 'post-reason',
'private', 'pubmail', 'visible', 'starred',
'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'network',
'unseen', 'deleted', 'origin', 'mention', 'global', 'network',
'title', 'content-warning', 'body', 'location', 'coord', 'app',
'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target',
'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network',
@ -655,7 +660,7 @@ class Item
$fields = ['uid', 'uri', 'parent-uri', 'id', 'deleted',
'uri-id', 'parent-uri-id',
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
'wall', 'private', 'forum_mode', 'origin', 'author-id'];
'wall', 'private', 'origin', 'author-id'];
$condition = ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']];
$params = ['order' => ['id' => false]];
$parent = Post::selectFirst($fields, $condition, $params);
@ -818,6 +823,15 @@ class Item
$item['inform'] = trim($item['inform'] ?? '');
$item['file'] = trim($item['file'] ?? '');
// Communities aren't working with the Diaspora protoccol
if (($uid != 0) && ($item['network'] == Protocol::DIASPORA)) {
$user = User::getById($uid, ['account-type']);
if ($user['account-type'] == Contact::TYPE_COMMUNITY) {
Logger::info('Community posts are not supported via Diaspora');
return 0;
}
}
// Items cannot be stored before they happen ...
if ($item['created'] > DateTimeFormat::utcNow()) {
$item['created'] = DateTimeFormat::utcNow();
@ -881,10 +895,15 @@ class Item
$item['parent-uri'] = $toplevel_parent['uri'];
$item['parent-uri-id'] = $toplevel_parent['uri-id'];
$item['deleted'] = $toplevel_parent['deleted'];
$item['allow_cid'] = $toplevel_parent['allow_cid'];
$item['allow_gid'] = $toplevel_parent['allow_gid'];
$item['deny_cid'] = $toplevel_parent['deny_cid'];
$item['deny_gid'] = $toplevel_parent['deny_gid'];
// Reshares have to keep their permissions to allow forums to work
if (!$item['origin'] || ($item['verb'] != Activity::ANNOUNCE)) {
$item['allow_cid'] = $toplevel_parent['allow_cid'];
$item['allow_gid'] = $toplevel_parent['allow_gid'];
$item['deny_cid'] = $toplevel_parent['deny_cid'];
$item['deny_gid'] = $toplevel_parent['deny_gid'];
}
$parent_origin = $toplevel_parent['origin'];
// Don't federate received participation messages
@ -905,15 +924,6 @@ class Item
$item['private'] = $toplevel_parent['private'];
}
/*
* Edge case. We host a public forum that was originally posted to privately.
* 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($toplevel_parent['forum_mode']) == 1) && ($toplevel_parent['private'] != self::PUBLIC)) {
$item['private'] = self::PUBLIC;
}
// If its a post that originated here then tag the thread as "mention"
if ($item['origin'] && $item['uid']) {
DBA::update('post-thread-user', ['mention' => true], ['uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]);
@ -1226,8 +1236,11 @@ class Item
return;
}
$self_contact = Contact::selectFirst(['id'], ['uid' => $item['uid'], 'self' => true]);
$self = !empty($self_contact) ? $self_contact['id'] : 0;
$cid = Contact::getIdForURL($author['url'], $item['uid']);
if (empty($cid) || !Contact::isSharing($cid, $item['uid'])) {
if (empty($cid) || (!Contact::isSharing($cid, $item['uid']) && ($cid != $self))) {
Logger::info('The resharer is not a following contact: quit', ['resharer' => $author['url'], 'uid' => $item['uid'], 'cid' => $cid]);
return;
}
@ -1398,7 +1411,7 @@ class Item
$is_reshare = ($item['gravity'] == GRAVITY_ACTIVITY) && ($item['verb'] == Activity::ANNOUNCE);
if ((($item['gravity'] == GRAVITY_PARENT) || $is_reshare) &&
DI::pConfig()->get($uid, 'system', 'accept_only_sharer') &&
DI::pConfig()->get($uid, 'system', 'accept_only_sharer') == self::COMPLETION_NONE &&
!Contact::isSharingByURL($item['author-link'], $uid) &&
!Contact::isSharingByURL($item['owner-link'], $uid)) {
Logger::info('Contact is not a follower, thread will not be stored', ['author' => $item['author-link'], 'uid' => $uid]);
@ -1406,9 +1419,15 @@ class Item
}
if ((($item['gravity'] == GRAVITY_COMMENT) || $is_reshare) && !Post::exists(['uri-id' => $item['thr-parent-id'], 'uid' => $uid])) {
// Only do an auto complete with the source uid "0" to prevent privavy problems
// Fetch the origin user for the post
$origin_uid = self::GetOriginUidForUriId($item['thr-parent-id'], $uid);
if (is_null($origin_uid)) {
Logger::info('Origin item was not found', ['uid' => $uid, 'uri-id' => $item['thr-parent-id']]);
return 0;
}
$causer = $item['causer-id'] ?: $item['author-id'];
$result = self::storeForUserByUriId($item['thr-parent-id'], $uid, ['causer-id' => $causer, 'post-reason' => self::PR_FETCHED]);
$result = self::storeForUserByUriId($item['thr-parent-id'], $uid, ['causer-id' => $causer, 'post-reason' => self::PR_FETCHED], $origin_uid);
Logger::info('Fetched thread parent', ['uri-id' => $item['thr-parent-id'], 'uid' => $uid, 'causer' => $causer, 'result' => $result]);
}
@ -1417,6 +1436,56 @@ class Item
return $stored;
}
/**
* Returns the origin uid of a post if the given user is allowed to see it.
*
* @param int $uriid
* @param int $uid
* @return int
*/
private static function GetOriginUidForUriId(int $uriid, int $uid)
{
if (Post::exists(['uri-id' => $uriid, 'uid' => $uid])) {
return $uid;
}
$post = Post::selectFirst(['uid', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'private'], ['uri-id' => $uriid, 'origin' => true]);
if (!empty($post)) {
if (in_array($post['private'], [Item::PUBLIC, Item::UNLISTED])) {
return $post['uid'];
}
$pcid = Contact::getPublicIdByUserId($uid);
if (empty($pcid)) {
return null;
}
foreach (Item::enumeratePermissions($post, true) as $receiver) {
if ($receiver == $pcid) {
return $post['uid'];
}
}
return null;
}
if (Post::exists(['uri-id' => $uriid, 'uid' => 0])) {
return 0;
}
// When the post belongs to a a forum then all forum users are allowed to access it
foreach (Tag::getByURIId($uriid, [Tag::MENTION, Tag::EXCLUSIVE_MENTION]) as $tag) {
if (DBA::exists('contact', ['uid' => $uid, 'nurl' => Strings::normaliseLink($tag['url']), 'contact-type' => Contact::TYPE_COMMUNITY])) {
$target_uid = User::getIdForURL($tag['url']);
if (!empty($target_uid)) {
return $target_uid;
}
}
}
return null;
}
/**
* Store a public item array for the given users
*
@ -1443,6 +1512,7 @@ class Item
return 0;
}
// Data from the "post-user" table
unset($item['id']);
unset($item['mention']);
unset($item['starred']);
@ -1451,11 +1521,14 @@ class Item
unset($item['pinned']);
unset($item['ignored']);
unset($item['pubmail']);
unset($item['forum_mode']);
unset($item['event-id']);
unset($item['hidden']);
unset($item['notification-type']);
unset($item['post-reason']);
// Data from the "post-delivery-data" table
unset($item['postopts']);
unset($item['inform']);
$item['uid'] = $uid;
$item['origin'] = 0;
@ -1875,7 +1948,7 @@ class Item
$owner = User::getOwnerDataById($uid);
if (!DBA::isResult($owner)) {
Logger::warning('User not found, quitting.', ['uid' => $uid]);
Logger::warning('User not found, quitting here.', ['uid' => $uid]);
return false;
}
@ -1884,85 +1957,57 @@ class Item
return false;
}
$item = Post::selectFirst(self::ITEM_FIELDLIST, ['id' => $item_id]);
$item = Post::selectFirst(self::ITEM_FIELDLIST, ['id' => $item_id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'origin' => false]);
if (!DBA::isResult($item)) {
Logger::warning('Post not found, quitting.', ['id' => $item_id]);
Logger::debug('Post is an activity or origin or not found at all, quitting here.', ['id' => $item_id]);
return false;
}
if ($item['wall'] || $item['origin'] || ($item['gravity'] != GRAVITY_PARENT)) {
Logger::debug('Wall item, origin item or no parent post, quitting here.', ['wall' => $item['wall'], 'origin' => $item['origin'], 'gravity' => $item['gravity'], 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
return false;
}
$tags = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]);
foreach ($tags as $tag) {
if (Strings::compareLink($owner['url'], $tag['url'])) {
$mention = true;
Logger::info('Mention found in tag.', ['url' => $tag['url'], 'uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
}
}
// This check can most likely be removed since we always are having the tags
if (!$mention) {
$cnt = preg_match_all('/[\@\!]\[url\=(.*?)\](.*?)\[\/url\]/ism', $item['body'], $matches, PREG_SET_ORDER);
if ($cnt) {
foreach ($matches as $mtch) {
if (Strings::compareLink($owner['url'], $mtch[1])) {
$mention = true;
Logger::notice('Mention found in body.', ['mention' => $mtch[2], 'uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
}
if ($item['gravity'] == GRAVITY_PARENT) {
$tags = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]);
foreach ($tags as $tag) {
if (Strings::compareLink($owner['url'], $tag['url'])) {
$mention = true;
Logger::info('Mention found in tag.', ['url' => $tag['url'], 'uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
}
}
if (!$mention) {
Logger::info('Top-level post without mention is deleted.', ['uri' => $item['uri'], $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
Post\User::delete(['uri-id' => $item['uri-id'], 'uid' => $item['uid']]);
return true;
}
$arr = ['item' => $item, 'user' => $owner];
Hook::callAll('tagged', $arr);
} else {
$tags = Tag::getByURIId($item['parent-uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]);
foreach ($tags as $tag) {
if (Strings::compareLink($owner['url'], $tag['url'])) {
$mention = true;
Logger::info('Mention found in parent tag.', ['url' => $tag['url'], 'uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
}
}
if (!$mention) {
Logger::debug('No mentions found in parent, quitting here.', ['id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
return false;
}
}
if (!$mention) {
Logger::info('Top-level post without mention is deleted.', ['uri' => $item['uri'], $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
Post\User::delete(['uri-id' => $item['uri-id'], 'uid' => $item['uid']]);
return true;
}
$arr = ['item' => $item, 'user' => $owner];
Hook::callAll('tagged', $arr);
Logger::info('Community post will be distributed', ['uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
self::performActivity($item['id'], 'announce', $uid);
/**
* All the following lines are only needed for private forums and compatibility to older systems without AP support.
* A possible way would be that the followers list of a forum would always be readable by all followers.
* So this would mean that the comment distribution could be done exactly for the intended audience.
* Or possibly we could store the receivers that had been in the "announce" message above and use this.
*/
// also reset all the privacy bits to the forum default permissions
if ($owner['allow_cid'] || $owner['allow_gid'] || $owner['deny_cid'] || $owner['deny_gid']) {
$private = self::PRIVATE;
} elseif (DI::pConfig()->get($owner['uid'], 'system', 'unlisted')) {
$private = self::UNLISTED;
if ($owner['page-flags'] == User::PAGE_FLAGS_PRVGROUP) {
$allow_cid = '';
$allow_gid = '<' . Group::FOLLOWERS . '>';
$deny_cid = '';
$deny_gid = '';
self::performActivity($item['id'], 'announce', $uid, $allow_cid, $allow_gid, $deny_cid, $deny_gid);
} else {
$private = self::PUBLIC;
self::performActivity($item['id'], 'announce', $uid);
}
$permissionSet = DI::permissionSet()->selectOrCreate(
DI::permissionSetFactory()->createFromString(
$owner['uid'],
$owner['allow_cid'],
$owner['allow_gid'],
$owner['deny_cid'],
$owner['deny_gid']
));
$forum_mode = ($owner['page-flags'] == User::PAGE_FLAGS_PRVGROUP) ? 2 : 1;
$fields = ['wall' => true, 'origin' => true, 'forum_mode' => $forum_mode, 'contact-id' => $owner['id'],
'owner-id' => Contact::getPublicIdByUserId($uid), 'private' => $private, 'psid' => $permissionSet->id];
self::update($fields, ['id' => $item['id']]);
Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => true], 'Notifier', Delivery::POST, (int)$item['uri-id'], (int)$item['uid']);
Logger::info('Community post had been distributed', ['uri' => $item['uri'], 'uid' => $uid, 'id' => $item_id, 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
return false;
}
@ -2325,12 +2370,17 @@ class Item
*
* Toggle activities as like,dislike,attend of an item
*
* @param int $item_id
* @param int $item_id
* @param string $verb
* Activity verb. One of
* like, unlike, dislike, undislike, attendyes, unattendyes,
* attendno, unattendno, attendmaybe, unattendmaybe,
* announce, unannouce
* @param int $uid
* @param string $allow_cid
* @param string $allow_gid
* @param string $deny_cid
* @param string $deny_gid
* @return bool
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
@ -2338,7 +2388,7 @@ class Item
* array $arr
* 'post_id' => ID of posted item
*/
public static function performActivity(int $item_id, string $verb, int $uid)
public static function performActivity(int $item_id, string $verb, int $uid, string $allow_cid = null, string $allow_gid = null, string $deny_cid = null, string $deny_gid = null)
{
if (empty($uid)) {
return false;
@ -2499,10 +2549,10 @@ class Item
'body' => $activity,
'verb' => $activity,
'object-type' => $objtype,
'allow_cid' => $item['allow_cid'],
'allow_gid' => $item['allow_gid'],
'deny_cid' => $item['deny_cid'],
'deny_gid' => $item['deny_gid'],
'allow_cid' => $allow_cid ?? $item['allow_cid'],
'allow_gid' => $allow_gid ?? $item['allow_gid'],
'deny_cid' => $deny_cid ?? $item['deny_cid'],
'deny_gid' => $deny_gid ?? $item['deny_gid'],
'visible' => 1,
'unseen' => 1,
];
@ -3163,30 +3213,20 @@ class Item
}
/**
* Is the given item array a post that is sent as starting post to a forum?
* Does the given uri-id belongs to a post that is sent as starting post to a forum?
*
* @param array $item
* @param array $owner
* @param int $uri_id
*
* @return boolean "true" when it is a forum post
*/
public static function isForumPost(array $item, array $owner = [])
public static function isForumPost(int $uri_id)
{
if (empty($owner)) {
$owner = User::getOwnerDataById($item['uid']);
if (empty($owner)) {
return false;
foreach (Tag::getByURIId($uri_id, [Tag::EXCLUSIVE_MENTION]) as $tag) {
if (DBA::exists('contact', ['uid' => 0, 'nurl' => Strings::normaliseLink($tag['url']), 'contact-type' => Contact::TYPE_COMMUNITY])) {
return true;
}
}
if (($item['author-id'] == $item['owner-id']) ||
($owner['id'] == $item['contact-id']) ||
($item['uri-id'] != $item['parent-uri-id']) ||
$item['origin']) {
return false;
}
return Contact::isForum($item['contact-id']);
return false;
}
/**

View file

@ -518,7 +518,7 @@ class Media
$condition = DBA::mergeConditions($condition, ['type' => $types]);
}
return DBA::selectToArray('post-media', [], $condition);
return DBA::selectToArray('post-media', [], $condition, ['order' => ['id']]);
}
/**

View file

@ -33,6 +33,7 @@ use Friendica\Model\Contact;
use Friendica\Model\Post;
use Friendica\Model\Subscription;
use Friendica\Model\Tag;
use Friendica\Model\User;
use Friendica\Navigation\Notifications;
use Friendica\Network\HTTPException;
use Friendica\Protocol\Activity;
@ -176,6 +177,16 @@ class UserNotification
return;
}
$user = User::getById($uid, ['account-type']);
if (in_array($user['account-type'], [User::ACCOUNT_TYPE_COMMUNITY, User::ACCOUNT_TYPE_RELAY])) {
return;
}
$author = Contact::getById($item['author-id'], ['contact-type']);
if (empty($author)) {
return;
}
$notification_type = self::TYPE_NONE;
if (self::checkShared($item, $uid)) {
@ -189,11 +200,16 @@ class UserNotification
$profiles = self::getProfileForUser($uid);
// Fetch all contacts for the given profiles
$contacts = [];
$contacts = [];
$iscommunity = false;
$ret = DBA::select('contact', ['id'], ['uid' => 0, 'nurl' => $profiles]);
$ret = DBA::select('contact', ['id', 'contact-type'], ['uid' => 0, 'nurl' => $profiles]);
while ($contact = DBA::fetch($ret)) {
$contacts[] = $contact['id'];
if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
$iscommunity = true;
}
}
DBA::close($ret);
@ -226,7 +242,7 @@ class UserNotification
}
}
if (self::checkDirectCommentedThread($item, $contacts)) {
if (!$iscommunity && self::checkDirectCommentedThread($item, $contacts)) {
$notification_type = $notification_type | self::TYPE_DIRECT_THREAD_COMMENT;
if (!$notified) {
self::insertNotificationByItem(self::TYPE_DIRECT_THREAD_COMMENT, $uid, $item);

View file

@ -48,10 +48,15 @@ class Tag
*/
const IMPLICIT_MENTION = 8;
/**
* An exclusive mention transfers the ownership of the post to the target account, usually a forum.
* An exclusive mention transmits the post only to the target account without transmitting it to the followers, usually a forum.
*/
const EXCLUSIVE_MENTION = 9;
const TO = 10;
const CC = 11;
const BTO = 12;
const BCC = 13;
const TAG_CHARACTER = [
self::HASHTAG => '#',
self::MENTION => '@',
@ -66,9 +71,8 @@ class Tag
* @param integer $type
* @param string $name
* @param string $url
* @param boolean $probing
*/
public static function store(int $uriid, int $type, string $name, string $url = '', $probing = true)
public static function store(int $uriid, int $type, string $name, string $url = '')
{
if ($type == self::HASHTAG) {
// Trim Unicode non-word characters
@ -77,7 +81,7 @@ class Tag
$tags = explode(self::TAG_CHARACTER[self::HASHTAG], $name);
if (count($tags) > 1) {
foreach ($tags as $tag) {
self::store($uriid, $type, $tag, $url, $probing);
self::store($uriid, $type, $tag, $url);
}
return;
}
@ -90,7 +94,7 @@ class Tag
$cid = 0;
$tagid = 0;
if (in_array($type, [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION])) {
if (in_array($type, [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION, self::TO, self::CC, self::BTO, self::BCC])) {
if (empty($url)) {
// No mention without a contact url
return;
@ -100,32 +104,13 @@ class Tag
Logger::notice('Wrong scheme in url', ['url' => $url, 'callstack' => System::callstack(20)]);
}
if (!$probing) {
$condition = ['nurl' => Strings::normaliseLink($url), 'uid' => 0, 'deleted' => false];
$contact = DBA::selectFirst('contact', ['id'], $condition, ['order' => ['id']]);
if (DBA::isResult($contact)) {
$cid = $contact['id'];
Logger::info('Got id for contact url', ['cid' => $cid, 'url' => $url]);
}
if (empty($cid)) {
$ssl_url = str_replace('http://', 'https://', $url);
$condition = ['`alias` IN (?, ?, ?) AND `uid` = ? AND NOT `deleted`', $url, Strings::normaliseLink($url), $ssl_url, 0];
$contact = DBA::selectFirst('contact', ['id'], $condition, ['order' => ['id']]);
if (DBA::isResult($contact)) {
$cid = $contact['id'];
Logger::info('Got id for contact alias', ['cid' => $cid, 'url' => $url]);
}
}
} else {
$cid = Contact::getIdForURL($url, 0, false);
Logger::info('Got id by probing', ['cid' => $cid, 'url' => $url]);
}
$cid = Contact::getIdForURL($url, 0, false);
Logger::debug('Got id for contact', ['cid' => $cid, 'url' => $url]);
if (empty($cid)) {
// The contact wasn't found in the system (most likely some dead account)
// We ensure that we only store a single entry by overwriting the previous name
Logger::info('Contact not found, updating tag', ['url' => $url, 'name' => $name]);
Logger::info('URL is not a known contact, updating tag', ['url' => $url, 'name' => $name]);
if (!DBA::exists('tag', ['name' => substr($name, 0, 96), 'url' => $url])) {
DBA::update('tag', ['name' => substr($name, 0, 96)], ['url' => $url]);
}
@ -133,10 +118,12 @@ class Tag
}
if (empty($cid)) {
if (($type != self::HASHTAG) && !empty($url) && ($url != $name)) {
$url = strtolower($url);
} else {
$url = '';
if (!in_array($type, [self::TO, self::CC, self::BTO, self::BCC])) {
if (($type != self::HASHTAG) && !empty($url) && ($url != $name)) {
$url = strtolower($url);
} else {
$url = '';
}
}
$tagid = self::getID($name, $url);

View file

@ -70,9 +70,7 @@ class Objects extends BaseModule
}
}
$item = Post::selectFirst(['id', 'uid', 'origin', 'author-link', 'changed', 'private', 'psid', 'gravity', 'deleted', 'parent-uri-id'],
['uri-id' => $itemuri['id']], ['order' => ['origin' => true]]);
$item = Post::selectFirst([], ['uri-id' => $itemuri['id'], 'origin' => true]);
if (!DBA::isResult($item)) {
throw new HTTPException\NotFoundException();
}
@ -81,25 +79,17 @@ class Objects extends BaseModule
if (!$validated) {
$requester = HTTPSignature::getSigner('', $_SERVER);
if (!empty($requester) && $item['origin']) {
$requester_id = Contact::getIdForURL($requester, $item['uid']);
if (!empty($requester_id)) {
$permissionSets = DI::permissionSet()->selectByContactId($requester_id, $item['uid']);
$psids = array_merge($permissionSets->column('id'), [PermissionSet::PUBLIC]);
$validated = in_array($item['psid'], $psids);
if (!empty($requester)) {
$receivers = Item::enumeratePermissions($item, false);
$receivers[] = $item['contact-id'];
$validated = in_array(Contact::getIdForURL($requester, $item['uid']), $receivers);
if (!$validated) {
$validated = in_array(Contact::getIdForURL($requester), $receivers);
}
}
}
if ($validated) {
// Valid items are original post or posted from this node (including in the case of a forum)
$validated = ($item['origin'] || (parse_url($item['author-link'], PHP_URL_HOST) == parse_url(DI::baseUrl()->get(), PHP_URL_HOST)));
if (!$validated && $item['deleted']) {
$validated = Post::exists(['origin' => true, 'uri-id' => $item['parent-uri-id']]);
}
}
if (!$validated) {
throw new HTTPException\NotFoundException();
}

View file

@ -36,12 +36,32 @@ class Accounts extends BaseApi
{
protected function delete(array $request = [])
{
$this->response->unsupported(Router::DELETE, $request);
self::checkAllowedScope(self::SCOPE_WRITE);
$request = $this->getRequest([
'account_ids' => [], // Array of account IDs to remove from the list
], $request);
if (empty($request['account_ids']) || empty($this->parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
return Group::removeMembers($this->parameters['id'], $request['account_ids']);
}
protected function post(array $request = [])
{
$this->response->unsupported(Router::POST, $request);
self::checkAllowedScope(self::SCOPE_WRITE);
$request = $this->getRequest([
'account_ids' => [], // Array of account IDs to add to the list
], $request);
if (empty($request['account_ids']) || empty($this->parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
return Group::addMembers($this->parameters['id'], $request['account_ids']);
}
/**

View file

@ -56,7 +56,7 @@ class Ownership extends BaseApi
BaseApi::checkAllowedScope(BaseApi::SCOPE_READ);
$uid = BaseApi::getCurrentUserID();
$groups = $this->dba->select('group', [], ['deleted' => false, 'uid' => $uid]);
$groups = $this->dba->select('group', [], ['deleted' => false, 'uid' => $uid, 'cid' => null]);
// loop through all groups
$lists = [];

View file

@ -93,6 +93,12 @@ class Receive extends BaseModule
$importer = User::getByGuid($this->parameters['guid']);
if ($importer['account-type'] == User::ACCOUNT_TYPE_COMMUNITY) {
// Communities aren't working with the Diaspora protoccol
// We throw an "accepted" here, so that the sender doesn't repeat the delivery
throw new HTTPException\AcceptedException();
}
$msg = $this->decodePost(false, $importer['prvkey'] ?? '');
$this->logger->info('Diaspora: Dispatching.');

View file

@ -288,9 +288,10 @@ class Photo extends BaseModule
}
}
If (($contact['uid'] != 0) && empty($contact['photo']) && empty($contact['avatar'])) {
if (!empty($contact['uid']) && empty($contact['photo']) && empty($contact['avatar'])) {
$contact = Contact::getByURL($contact['url'], false, ['avatar', 'photo', 'xmpp', 'addr']);
}
if (!empty($contact['photo']) && !empty($contact['avatar'])) {
// Fetch photo directly
$resourceid = MPhoto::ridFromURI($contact['photo']);

View file

@ -118,7 +118,7 @@ class Status extends BaseProfile
$commvisitor = $commpage && $remote_contact;
DI::page()['aside'] .= Widget::postedByYear(DI::baseUrl() . '/profile/' . $profile['nickname'] . '/status', $profile['profile_uid'] ?? 0, true);
DI::page()['aside'] .= Widget::categories(DI::baseUrl() . '/profile/' . $profile['nickname'] . '/status', XML::escape($category));
DI::page()['aside'] .= Widget::categories($profile['uid'], DI::baseUrl() . '/profile/' . $profile['nickname'] . '/status', $category);
DI::page()['aside'] .= Widget::tagCloud($profile['uid']);
if (Security::canWriteToUserWall($profile['uid'])) {

View file

@ -82,7 +82,7 @@ class Notification extends BaseFactory implements ICanCreateFromTableRow
{
$message = [];
$causer = $author = Contact::getById($Notification->actorId, ['id', 'name', 'url', 'pending']);
$causer = $author = Contact::getById($Notification->actorId, ['id', 'name', 'url', 'contact-type', 'pending']);
if (empty($causer)) {
$this->logger->info('Causer not found', ['contact' => $Notification->actorId]);
return $message;
@ -124,7 +124,7 @@ class Notification extends BaseFactory implements ICanCreateFromTableRow
}
if (in_array($Notification->type, [Post\UserNotification::TYPE_COMMENT_PARTICIPATION, Post\UserNotification::TYPE_ACTIVITY_PARTICIPATION, Post\UserNotification::TYPE_SHARED])) {
$author = Contact::getById($item['author-id'], ['id', 'name', 'url']);
$author = Contact::getById($item['author-id'], ['id', 'name', 'url', 'contact-type']);
if (empty($author)) {
$this->logger->info('Author not found', ['author' => $item['author-id']]);
return $message;

View file

@ -71,7 +71,7 @@ class ScheduledStatus extends BaseDataTransferObject
'media_ids' => $media_ids,
'sensitive' => null,
'spoiler_text' => $parameters['item']['title'] ?? '',
'visibility' => $visibility[$parameters['item']['private']],
'visibility' => $visibility[$parameters['item']['private'] ?? 1],
'scheduled_at' => $this->scheduled_at,
'poll' => null,
'idempotency' => null,

View file

@ -416,12 +416,6 @@ class Post
$direction = [];
if (!empty($item['direction'])) {
$direction = $item['direction'];
} elseif (DI::config()->get('debug', 'show_direction')) {
$conversation = DBA::selectFirst('conversation', ['direction'], ['item-uri' => $item['uri']]);
if (!empty($conversation['direction']) && in_array($conversation['direction'], [1, 2])) {
$direction_title = [1 => DI::l10n()->t('Pushed'), 2 => DI::l10n()->t('Pulled')];
$direction = ['direction' => $conversation['direction'], 'title' => $direction_title[$conversation['direction']]];
}
}
$languages = [];

View file

@ -306,7 +306,7 @@ class Processor
} else {
// Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts
$item['causer-link'] = $item['owner-link'];
$item['causer-id'] = $item['owner-id'];
$item['causer-id'] = $item['owner-id'];
Logger::info('Use actor as causer.', ['id' => $item['owner-id'], 'actor' => $item['owner-link']]);
}
@ -526,6 +526,8 @@ class Processor
self::storeFromBody($item);
self::storeTags($item['uri-id'], $activity['tags']);
self::storeReceivers($item['uri-id'], $activity['receiver_urls'] ?? []);
$item['location'] = $activity['location'];
if (!empty($activity['latitude']) && !empty($activity['longitude'])) {
@ -642,10 +644,21 @@ class Processor
continue;
}
if (!($item['isForum'] ?? false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT) &&
($item['post-reason'] == Item::PR_BCC) && !Contact::isSharingByURL($activity['author'], $receiver)) {
Logger::info('Top level post via BCC from a non sharer, ignoring', ['uid' => $receiver, 'contact' => $item['contact-id']]);
continue;
if (!($item['isForum'] ?? false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT) && !Contact::isSharingByURL($activity['author'], $receiver)) {
if ($item['post-reason'] == Item::PR_BCC) {
Logger::info('Top level post via BCC from a non sharer, ignoring', ['uid' => $receiver, 'contact' => $item['contact-id']]);
continue;
}
if (
!empty($activity['thread-children-type'])
&& in_array($activity['thread-children-type'], Receiver::ACTIVITY_TYPES)
&& DI::pConfig()->get($receiver, 'system', 'accept_only_sharer') != Item::COMPLETION_LIKE
) {
Logger::info('Top level post from thread completion from a non sharer had been initiated via an activity, ignoring',
['type' => $activity['thread-children-type'], 'user' => $item['uid'], 'causer' => $item['causer-link'], 'author' => $activity['author'], 'url' => $item['uri']]);
continue;
}
}
$is_forum = false;
@ -657,7 +670,7 @@ class Processor
}
}
if (!$is_forum && DI::pConfig()->get($receiver, 'system', 'accept_only_sharer', false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) {
if (!$is_forum && DI::pConfig()->get($receiver, 'system', 'accept_only_sharer') == Item::COMPLETION_NONE && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) {
$skip = !Contact::isSharingByURL($activity['author'], $receiver);
if ($skip && (($activity['type'] == 'as:Announce') || ($item['isForum'] ?? false))) {
@ -745,6 +758,22 @@ class Processor
}
}
public static function storeReceivers(int $uriid, array $receivers)
{
foreach (['as:to' => Tag::TO, 'as:cc' => Tag::CC, 'as:bto' => Tag::BTO, 'as:bcc' => Tag::BCC] as $element => $type) {
if (!empty($receivers[$element])) {
foreach ($receivers[$element] as $receiver) {
if ($receiver == ActivityPub::PUBLIC_COLLECTION) {
$name = Receiver::PUBLIC_COLLECTION;
} else {
$name = trim(parse_url($receiver, PHP_URL_PATH), '/');
}
Tag::store($uriid, $type, $name, $receiver);
}
}
}
}
/**
* Creates an mail post
*
@ -887,6 +916,10 @@ class Processor
$ldactivity['thread-completion'] = Contact::getIdForURL($actor);
}
if (!empty($child['type'])) {
$ldactivity['thread-children-type'] = $child['type'];
}
if (!empty($relay_actor) && !self::acceptIncomingMessage($ldactivity, $object['id'])) {
return '';
}

View file

@ -263,7 +263,9 @@ class Receiver
{
$id = JsonLD::fetchElement($activity, '@id');
if (!empty($id) && !$trust_source) {
$fetched_activity = ActivityPub::fetchContent($id, $uid ?? 0);
$fetch_uid = $uid ?: self::getBestUserForActivity($activity);
$fetched_activity = ActivityPub::fetchContent($id, $fetch_uid);
if (!empty($fetched_activity)) {
$object = JsonLD::compact($fetched_activity);
$fetched_id = JsonLD::fetchElement($object, '@id');
@ -295,19 +297,25 @@ class Receiver
$reception_types[$data['uid']] = $data['type'] ?? self::TARGET_UNKNOWN;
}
$urls = self::getReceiverURL($activity);
// When it is a delivery to a personal inbox we add that user to the receivers
if (!empty($uid)) {
$additional = [$uid => $uid];
$receivers = array_replace($receivers, $additional);
if (empty($activity['thread-completion']) && (empty($reception_types[$uid]) || in_array($reception_types[$uid], [self::TARGET_UNKNOWN, self::TARGET_FOLLOWER, self::TARGET_ANSWER, self::TARGET_GLOBAL]))) {
$reception_types[$uid] = self::TARGET_BCC;
$owner = User::getOwnerDataById($uid);
if (!empty($owner['url'])) {
$urls['as:bcc'][] = $owner['url'];
}
}
} else {
// We possibly need some user to fetch private content,
// so we fetch the first out ot the list.
$uid = self::getFirstUserFromReceivers($receivers);
}
// We possibly need some user to fetch private content,
// so we fetch one out of the receivers if no uid is provided.
$fetch_uid = $uid ?: self::getBestUserForActivity($activity);
$object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
if (empty($object_id)) {
Logger::info('No object found');
@ -319,11 +327,11 @@ class Receiver
return [];
}
$object_type = self::fetchObjectType($activity, $object_id, $uid);
$object_type = self::fetchObjectType($activity, $object_id, $fetch_uid);
// Fetch the activity on Lemmy "Announce" messages (announces of activities)
if (($type == 'as:Announce') && in_array($object_type, array_merge(self::ACTIVITY_TYPES, ['as:Delete', 'as:Undo', 'as:Update']))) {
$data = ActivityPub::fetchContent($object_id, $uid);
$data = ActivityPub::fetchContent($object_id, $fetch_uid);
if (!empty($data)) {
$type = $object_type;
$activity = JsonLD::compact($data);
@ -331,7 +339,7 @@ class Receiver
// Some variables need to be refetched since the activity changed
$actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
$object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
$object_type = self::fetchObjectType($activity, $object_id, $uid);
$object_type = self::fetchObjectType($activity, $object_id, $fetch_uid);
}
}
@ -348,7 +356,7 @@ class Receiver
// Fetch the content only on activities where this matters
// We can receive "#emojiReaction" when fetching content from Hubzilla systems
// Always fetch on "Announce"
$object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source && ($type != 'as:Announce'), $uid);
$object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source && ($type != 'as:Announce'), $fetch_uid);
if (empty($object_data)) {
Logger::info("Object data couldn't be processed");
return [];
@ -396,7 +404,7 @@ class Receiver
// An Undo is done on the object of an object, so we need that type as well
if (($type == 'as:Undo') && !empty($object_data['object_object'])) {
$object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object'], $uid);
$object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object'], $fetch_uid);
}
}
@ -406,6 +414,12 @@ class Receiver
$object_data['object_type'] = $object_type;
}
foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) {
if (!empty($urls[$element])) {
$object_data['receiver_urls'][$element] = array_unique(array_merge($object_data['receiver_urls'][$element] ?? [], $urls[$element]));
}
}
$object_data['type'] = $type;
$object_data['actor'] = $actor;
$object_data['item_receiver'] = $receivers;
@ -515,6 +529,9 @@ class Receiver
if (!empty($activity['thread-completion'])) {
$object_data['thread-completion'] = $activity['thread-completion'];
}
if (!empty($activity['thread-children-type'])) {
$object_data['thread-children-type'] = $activity['thread-children-type'];
}
// Internal flag for posts that arrived via relay
if (!empty($activity['from-relay'])) {
@ -640,6 +657,61 @@ class Receiver
}
}
/**
* Fetch a user id from an activity array
*
* @param array $activity
* @param string $actor
*
* @return int user id
*/
public static function getBestUserForActivity(array $activity)
{
$uid = 0;
$actor = JsonLD::fetchElement($activity, 'as:actor', '@id') ?? '';
$receivers = self::getReceivers($activity, $actor);
foreach ($receivers as $receiver) {
if ($receiver['type'] == self::TARGET_GLOBAL) {
return 0;
}
if (empty($uid) || ($receiver['type'] == self::TARGET_TO)) {
$uid = $receiver['uid'];
}
}
// When we haven't found any user yet, we just chose a user who most likely could have access to the content
if (empty($uid)) {
$contact = Contact::selectFirst(['uid'], ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND]]);
if (!empty($contact['uid'])) {
$uid = $contact['uid'];
}
}
return $uid;
}
public static function getReceiverURL($activity)
{
$urls = [];
foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) {
$receiver_list = JsonLD::fetchElementArray($activity, $element, '@id');
if (empty($receiver_list)) {
continue;
}
foreach ($receiver_list as $receiver) {
if ($receiver == self::PUBLIC_COLLECTION) {
$receiver = ActivityPub::PUBLIC_COLLECTION;
}
$urls[$element][] = $receiver;
}
}
return $urls;
}
/**
* Fetch the receiver list from an activity array
*
@ -1469,7 +1541,8 @@ class Receiver
$reception_types[$data['uid']] = $data['type'] ?? 0;
}
$object_data['receiver'] = $receivers;
$object_data['receiver_urls'] = self::getReceiverURL($object);
$object_data['receiver'] = $receivers;
$object_data['reception_type'] = $reception_types;
$object_data['unlisted'] = in_array(-1, $object_data['receiver']);

View file

@ -184,7 +184,7 @@ class Transmitter
// Allow fetching the contact list when the requester is part of the list.
if (($owner['page-flags'] == User::PAGE_FLAGS_PRVGROUP) && !empty($requester)) {
$show_contacts = DBA::exists('contact', ['nurl' => Strings::normaliseLink($requester), 'rel' => $rel]);
$show_contacts = DBA::exists('contact', ['nurl' => Strings::normaliseLink($requester), 'uid' => $owner['uid'], 'blocked' => false]);
}
if (!$show_contacts) {
@ -424,7 +424,7 @@ class Transmitter
}
/**
* Returns an array with permissions of a given item array
* Returns an array with permissions of the thread parent of the given item array
*
* @param array $item
*
@ -432,34 +432,25 @@ class Transmitter
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function fetchPermissionBlockFromConversation($item)
public static function fetchPermissionBlockFromThreadParent($item)
{
if (empty($item['thr-parent'])) {
if (empty($item['thr-parent-id'])) {
return [];
}
$condition = ['item-uri' => $item['thr-parent'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB];
$conversation = DBA::selectFirst('conversation', ['source'], $condition);
if (!DBA::isResult($conversation)) {
$parent = Post::selectFirstPost(['author-link'], ['uri-id' => $item['thr-parent-id']]);
if (empty($parent)) {
return [];
}
$permissions = [
'to' => [],
'to' => [$parent['author-link']],
'cc' => [],
'bto' => [],
'bcc' => [],
];
$activity = json_decode($conversation['source'], true);
$actor = JsonLD::fetchElement($activity, 'actor', 'id');
if (!empty($actor)) {
$permissions['to'][] = $actor;
$profile = APContact::getByURL($actor);
} else {
$profile = [];
}
$parent_profile = APContact::getByURL($parent['author-link']);
$item_profile = APContact::getByURL($item['author-link']);
$exclude[] = $item['author-link'];
@ -468,26 +459,15 @@ class Transmitter
$exclude[] = $item['owner-link'];
}
foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
if (empty($activity[$element])) {
continue;
}
if (is_string($activity[$element])) {
$activity[$element] = [$activity[$element]];
}
foreach ($activity[$element] as $receiver) {
if (empty($receiver)) {
continue;
}
if (!empty($profile['followers']) && $receiver == $profile['followers'] && !empty($item_profile['followers'])) {
$permissions[$element][] = $item_profile['followers'];
} elseif (!in_array($receiver, $exclude)) {
$permissions[$element][] = $receiver;
}
$type = [Tag::TO => 'to', Tag::CC => 'cc', Tag::BTO => 'bto', Tag::BCC => 'bcc'];
foreach (Tag::getByURIId($item['thr-parent-id'], [Tag::TO, Tag::CC, Tag::BTO, Tag::BCC]) as $receiver) {
if (!empty($parent_profile['followers']) && $receiver['url'] == $parent_profile['followers'] && !empty($item_profile['followers'])) {
$permissions[$type[$receiver['type']]][] = $item_profile['followers'];
} elseif (!in_array($receiver['url'], $exclude)) {
$permissions[$type[$receiver['type']]][] = $receiver['url'];
}
}
return $permissions;
}
@ -509,28 +489,33 @@ class Transmitter
/**
* Creates an array of permissions from an item thread
*
* @param array $item Item array
* @param boolean $blindcopy addressing via "bcc" or "cc"?
* @param integer $last_id Last item id for adding receivers
* @param boolean $forum_post "true" means that we are sending content to a forum
* @param array $item Item array
* @param boolean $blindcopy addressing via "bcc" or "cc"?
* @param integer $last_id Last item id for adding receivers
*
* @return array with permission data
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function createPermissionBlockForItem($item, $blindcopy, $last_id = 0, $forum_post = false)
private static function createPermissionBlockForItem($item, $blindcopy, $last_id = 0)
{
if ($last_id == 0) {
$last_id = $item['id'];
}
$always_bcc = false;
$is_forum = false;
$follower = '';
// Check if we should always deliver our stuff via BCC
if (!empty($item['uid'])) {
$profile = User::getOwnerDataById($item['uid']);
if (!empty($profile)) {
$always_bcc = $profile['hide-friends'];
$owner = User::getOwnerDataById($item['uid']);
if (!empty($owner)) {
$always_bcc = $owner['hide-friends'];
$is_forum = ($owner['account-type'] == User::ACCOUNT_TYPE_COMMUNITY) && $owner['manually-approve'];
$profile = APContact::getByURL($owner['url'], false);
$follower = $profile['followers'] ?? '';
}
}
@ -568,7 +553,7 @@ class Transmitter
$data['cc'][] = $announce['actor']['url'];
}
$data = array_merge($data, self::fetchPermissionBlockFromConversation($item));
$data = array_merge($data, self::fetchPermissionBlockFromThreadParent($item));
// Check if the item is completely public or unlisted
if ($item['private'] == Item::PUBLIC) {
@ -600,23 +585,34 @@ class Transmitter
continue;
}
if (!empty($profile = APContact::getByURL($contact['url'], false))) {
$profile = APContact::getByURL($term['url'], false);
if (!empty($profile)) {
if ($term['type'] == Tag::EXCLUSIVE_MENTION) {
$exclusive = true;
if (!empty($profile['followers']) && ($profile['type'] == 'Group')) {
$data['cc'][] = $profile['followers'];
}
}
$data['to'][] = $profile['url'];
}
}
}
foreach ($receiver_list as $receiver) {
$contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol', 'gsid'], ['id' => $receiver, 'network' => Protocol::FEDERATED]);
if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) {
continue;
}
if ($is_forum && !$exclusive && !empty($follower)) {
$data['cc'][] = $follower;
} elseif (!$exclusive) {
foreach ($receiver_list as $receiver) {
$contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol', 'gsid'], ['id' => $receiver, 'network' => Protocol::FEDERATED]);
if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) {
continue;
}
if (!empty($profile = APContact::getByURL($contact['url'], false))) {
if ($contact['hidden'] || $always_bcc) {
$data['bcc'][] = $profile['url'];
} else {
$data['cc'][] = $profile['url'];
if (!empty($profile = APContact::getByURL($contact['url'], false))) {
if ($contact['hidden'] || $always_bcc) {
$data['bcc'][] = $profile['url'];
} else {
$data['cc'][] = $profile['url'];
}
}
}
}
@ -643,9 +639,7 @@ class Transmitter
}
} elseif (!$exclusive) {
// Public thread parent post always are directed to the followers.
// This mustn't be done by posts that are directed to forum servers via the exclusive mention.
// But possibly in that case we could add the "followers" collection of the forum to the message.
if (($item['private'] != Item::PRIVATE) && !$forum_post) {
if ($item['private'] != Item::PRIVATE) {
$data['cc'][] = $actor_profile['followers'];
}
}
@ -707,6 +701,19 @@ class Transmitter
unset($receivers['bcc']);
}
foreach (['to' => Tag::TO, 'cc' => Tag::CC, 'bcc' => Tag::BCC] as $element => $type) {
if (!empty($receivers[$element])) {
foreach ($receivers[$element] as $receiver) {
if ($receiver == ActivityPub::PUBLIC_COLLECTION) {
$name = Receiver::PUBLIC_COLLECTION;
} else {
$name = trim(parse_url($receiver, PHP_URL_PATH), '/');
}
Tag::store($item['uri-id'], $type, $name, $receiver);
}
}
}
return $receivers;
}
@ -811,18 +818,17 @@ class Transmitter
/**
* Fetches an array of inboxes for the given item and user
*
* @param array $item Item array
* @param integer $uid User ID
* @param boolean $personal fetch personal inboxes
* @param integer $last_id Last item id for adding receivers
* @param boolean $forum_post "true" means that we are sending content to a forum
* @param array $item Item array
* @param integer $uid User ID
* @param boolean $personal fetch personal inboxes
* @param integer $last_id Last item id for adding receivers
* @return array with inboxes
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function fetchTargetInboxes($item, $uid, $personal = false, $last_id = 0, $forum_post = false)
public static function fetchTargetInboxes($item, $uid, $personal = false, $last_id = 0)
{
$permissions = self::createPermissionBlockForItem($item, true, $last_id, $forum_post);
$permissions = self::createPermissionBlockForItem($item, true, $last_id);
if (empty($permissions)) {
return [];
}
@ -1079,20 +1085,6 @@ class Transmitter
return false;
}
// In case of a forum post ensure to return the original post if author and forum are on the same machine
if (($item['gravity'] == GRAVITY_PARENT) && !empty($item['forum_mode'])) {
$author = Contact::getById($item['author-id'], ['nurl']);
if (!empty($author['nurl'])) {
$self = Contact::selectFirst(['uid'], ['nurl' => $author['nurl'], 'self' => true]);
if (!empty($self['uid'])) {
$forum_item = Post::selectFirst(Item::DELIVER_FIELDLIST, ['uri-id' => $item['uri-id'], 'uid' => $self['uid']]);
if (DBA::isResult($forum_item)) {
$item = $forum_item;
}
}
}
}
if (empty($item['uri-id'])) {
Logger::warning('Item without uri-id', ['item' => $item]);
return false;

View file

@ -1557,22 +1557,11 @@ class DFRN
// was the top-level post for this action written by somebody on this site?
// Specifically, the recipient?
$parent = Post::selectFirst(['forum_mode', 'wall'],
$parent = Post::selectFirst(['wall'],
["`uri` = ? AND `uid` = ?" . $sql_extra, $item["thr-parent"], $importer["importer_uid"]]);
$is_a_remote_action = DBA::isResult($parent);
/*
* Does this have the characteristics of a community or private group action?
* If it's an action to a wall post on a community/prvgroup page it's a
* valid community action. Also forum_mode makes it valid for sure.
* If neither, it's not.
*/
if ($is_a_remote_action && $community && (!$parent["forum_mode"]) && (!$parent["wall"])) {
$is_a_remote_action = false;
Logger::notice("not a community action");
}
if ($is_a_remote_action) {
return DFRN::REPLY_RC;
} else {

View file

@ -858,10 +858,6 @@ class Diaspora
} elseif (($contact["rel"] == Contact::SHARING) || ($contact["rel"] == Contact::FRIEND)) {
// Yes, then it is fine.
return true;
// Is it a post to a community?
} elseif (($contact["rel"] == Contact::FOLLOWER) && ($importer['account-type'] == User::ACCOUNT_TYPE_COMMUNITY)) {
// That's good
return true;
// Is the message a global user or a comment?
} elseif (($importer["uid"] == 0) || $is_comment) {
// Messages for the global users and comments are always accepted
@ -3473,9 +3469,8 @@ class Diaspora
private static function prependParentAuthorMention($body, $profile_url)
{
$profile = Contact::getByURL($profile_url, false, ['addr', 'name', 'contact-type']);
$profile = Contact::getByURL($profile_url, false, ['addr', 'name']);
if (!empty($profile['addr'])
&& $profile['contact-type'] != Contact::TYPE_COMMUNITY
&& !strstr($body, $profile['addr'])
&& !strstr($body, $profile_url)
) {

View file

@ -494,19 +494,22 @@ class OStatus
if ($initialize && (count(self::$itemlist) > 0)) {
if (self::$itemlist[0]['uri'] == self::$itemlist[0]['thr-parent']) {
$uid = self::$itemlist[0]['uid'];
// We will import it everytime, when it is started by our contacts
$valid = Contact::isSharingByURL(self::$itemlist[0]['author-link'], self::$itemlist[0]['uid']);
$valid = Contact::isSharingByURL(self::$itemlist[0]['author-link'], $uid);
if (!$valid) {
// If not, then it depends on this setting
$valid = ((self::$itemlist[0]['uid'] == 0) || !DI::pConfig()->get(self::$itemlist[0]['uid'], 'system', 'accept_only_sharer', false));
$valid = !$uid || DI::pConfig()->get($uid, 'system', 'accept_only_sharer') != Item::COMPLETION_NONE;
if ($valid) {
Logger::info("Item with uri ".self::$itemlist[0]['uri']." will be imported due to the system settings.");
}
} else {
Logger::info("Item with uri ".self::$itemlist[0]['uri']." belongs to a contact (".self::$itemlist[0]['contact-id']."). It will be imported.");
}
if ($valid) {
if ($valid && DI::pConfig()->get($uid, 'system', 'accept_only_sharer') != Item::COMPLETION_LIKE) {
// Never post a thread when the only interaction by our contact was a like
$valid = false;
$verbs = [Activity::POST, Activity::SHARE];
@ -1728,6 +1731,7 @@ class OStatus
if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) {
$contact = Contact::getByURL($item['author-link']) ?: $owner;
$contact['nickname'] = $contact['nickname'] ?? $contact['nick'];
$author = self::addAuthor($doc, $contact, false);
$entry->appendChild($author);
}

View file

@ -58,5 +58,7 @@ class FriendicaSmarty extends Smarty
// Don't report errors so verbosely
$this->error_reporting = E_ALL & ~E_NOTICE;
$this->muteUndefinedOrNullWarnings();
}
}

View file

@ -627,7 +627,8 @@ class HTTPSignature
if (!empty($created)) {
$current = time();
if ($created > $current) {
// Calculate with a grace period of 60 seconds to avoid slight time differences between the servers
if (($created - 60) > $current) {
Logger::notice('Signature created in the future', ['created' => date(DateTimeFormat::MYSQL, $created), 'expired' => date(DateTimeFormat::MYSQL, $expired), 'current' => date(DateTimeFormat::MYSQL, $current)]);
return false;
}

View file

@ -184,12 +184,14 @@ class Images
return $data;
}
$data = DI::cache()->get($url);
$cacheKey = 'getInfoFromURL:' . sha1($url);
$data = DI::cache()->get($cacheKey);
if (empty($data) || !is_array($data)) {
$data = self::getInfoFromURL($url);
DI::cache()->set($url, $data);
DI::cache()->set($cacheKey, $data);
}
return $data;

View file

@ -46,7 +46,6 @@ class Delivery
const DELETION = 'drop';
const POST = 'wall-new';
const POKE = 'poke';
const UPLINK = 'uplink';
const REMOVAL = 'removeme';
const PROFILEUPDATE = 'profileupdate';

View file

@ -153,7 +153,7 @@ class Notifier
}
// Should the post be transmitted to Diaspora?
$diaspora_delivery = true;
$diaspora_delivery = ($owner['account-type'] != User::ACCOUNT_TYPE_COMMUNITY);
// If this is a public conversation, notify the feed hub
$public_message = true;
@ -223,10 +223,6 @@ class Notifier
$relay_to_owner = true;
}
if (($cmd === Delivery::UPLINK) && (intval($parent['forum_mode']) == 1) && !$top_level) {
$relay_to_owner = true;
}
// until the 'origin' flag has been in use for several months
// we will just use it as a fallback test
// later we will be able to use it as the primary test of whether or not to relay.
@ -239,13 +235,13 @@ class Notifier
}
// Special treatment for forum posts
if (Item::isForumPost($target_item, $owner)) {
if (Item::isForumPost($target_item['uri-id'])) {
$relay_to_owner = true;
$direct_forum_delivery = true;
}
// Avoid that comments in a forum thread are sent to OStatus
if (Item::isForumPost($parent, $owner)) {
if (Item::isForumPost($parent['uri-id'])) {
$direct_forum_delivery = true;
}
@ -333,15 +329,6 @@ class Notifier
$deny_people = $aclFormatter->expand($parent['deny_cid']);
$deny_groups = Group::expand($uid, $aclFormatter->expand($parent['deny_gid']));
// if our parent is a public forum (forum_mode == 1), uplink to the origional author causing
// a delivery fork. private groups (forum_mode == 2) do not uplink
/// @todo Possibly we should not uplink when the author is the forum itself?
if ((intval($parent['forum_mode']) == 1) && !$top_level && ($cmd !== Delivery::UPLINK)
&& ($target_item['verb'] != Activity::ANNOUNCE)) {
Worker::add($a->getQueueValue('priority'), 'Notifier', Delivery::UPLINK, $post_uriid, $sender_uid);
}
foreach ($items as $item) {
$recipients[] = $item['contact-id'];
// pull out additional tagged people to notify (if public message)
@ -742,6 +729,14 @@ class Notifier
$uid = $target_item['contact-uid'] ?: $target_item['uid'];
// Update the locally stored follower list when we deliver to a forum
foreach (Tag::getByURIId($target_item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]) as $tag) {
$target_contact = Contact::getByURL(Strings::normaliseLink($tag['url']), null, [], $uid);
if ($target_contact && $target_contact['contact-type'] == Contact::TYPE_COMMUNITY && $target_contact['manually-approve']) {
Group::updateMembersForForum($target_contact['id']);
}
}
if ($target_item['origin']) {
$inboxes = ActivityPub\Transmitter::fetchTargetInboxes($target_item, $uid);
@ -751,9 +746,6 @@ class Notifier
}
Logger::info('Origin item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . ' will be distributed.');
} elseif (Item::isForumPost($target_item, $owner)) {
$inboxes = ActivityPub\Transmitter::fetchTargetInboxes($target_item, $uid, false, 0, true);
Logger::info('Forum item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . ' will be distributed.');
} elseif (!DBA::exists('conversation', ['item-uri' => $target_item['uri'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB])) {
Logger::info('Remote item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . ' is no AP post. It will not be distributed.');
return ['count' => 0, 'contacts' => []];
@ -813,15 +805,4 @@ class Notifier
return ['count' => $delivery_queue_count, 'contacts' => $contacts];
}
/**
* Check if the delivered item is a forum post
*
* @param array $item
* @return boolean
*/
public static function isForumPost(array $item)
{
return ($item['gravity'] == GRAVITY_PARENT) && !empty($item['forum_mode']);
}
}

View file

@ -55,7 +55,7 @@
use Friendica\Database\DBA;
if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1451);
define('DB_UPDATE_VERSION', 1452);
}
return [
@ -1301,7 +1301,7 @@ return [
"wall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "This item was posted to the wall of uid"],
"mention" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""],
"pubmail" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""],
"forum_mode" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""],
"forum_mode" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "Deprecated"],
"contact-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id"], "comment" => "contact.id"],
"unseen" => ["type" => "boolean", "not null" => "1", "default" => "1", "comment" => "post has not been seen"],
"hidden" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Marker to hide the post from the user"],

View file

@ -91,7 +91,6 @@
"deleted" => ["post-user", "deleted"],
"origin" => ["post-user", "origin"],
"parent-origin" => ["post-thread-user", "origin"],
"forum_mode" => ["post-thread-user", "forum_mode"],
"mention" => ["post-thread-user", "mention"],
"global" => ["post-user", "global"],
"network" => ["post-user", "network"],
@ -250,7 +249,6 @@
"unseen" => ["post-thread-user", "unseen"],
"deleted" => ["post-user", "deleted"],
"origin" => ["post-thread-user", "origin"],
"forum_mode" => ["post-thread-user", "forum_mode"],
"mention" => ["post-thread-user", "mention"],
"global" => ["post-user", "global"],
"network" => ["post-thread-user", "network"],

View file

@ -616,10 +616,6 @@ return [
// Logs every call to /inbox as a JSON file in Friendica's temporary directory
'ap_inbox_log' => false,
// show_direction (Boolean)
// Display if a post had been fetched or had been pushed towards our server
'show_direction' => false,
// total_ap_delivery (Boolean)
// Deliver via AP to every possible receiver and we suppress the delivery to these contacts with other protocols
'total_ap_delivery' => false,

View file

@ -102,7 +102,7 @@ class ConfigConsoleTest extends ConsoleTest
$console->setArgument(0, 'config');
$console->setArgument(1, 'test');
$txt = $this->dumpExecute($console);
self::assertEquals("config.test => \n", $txt);
self::assertEquals("config.test => NULL\n", $txt);
}
public function testSetArrayValue()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -326,8 +326,6 @@ $a->strings['Name too short.'] = 'Der Name ist zu kurz.';
$a->strings['Wrong Password.'] = 'Falsches Passwort';
$a->strings['Invalid email.'] = 'Ungültige E-Mail-Adresse.';
$a->strings['Cannot change to that email.'] = 'Ändern der E-Mail nicht möglich. ';
$a->strings['Private forum has no privacy permissions. Using default privacy group.'] = 'Für das private Forum sind keine Zugriffsrechte eingestellt. Die voreingestellte Gruppe für neue Kontakte wird benutzt.';
$a->strings['Private forum has no privacy permissions and no default privacy group.'] = 'Für das private Forum sind keine Zugriffsrechte eingestellt, und es gibt keine voreingestellte Gruppe für neue Kontakte.';
$a->strings['Settings were not updated.'] = 'Einstellungen nicht aktualisiert';
$a->strings['Connected Apps'] = 'Verbundene Programme';
$a->strings['Name'] = 'Name';
@ -347,8 +345,11 @@ $a->strings['Email access is disabled on this site.'] = 'Zugriff auf E-Mails fü
$a->strings['None'] = 'Keine';
$a->strings['Social Networks'] = 'Soziale Netzwerke';
$a->strings['General Social Media Settings'] = 'Allgemeine Einstellungen zu Sozialen Medien';
$a->strings['Accept only top level posts by contacts you follow'] = 'Ausschließlich Unterhaltungen von meinen Kontakten anzeigen';
$a->strings['The system does an auto completion of threads when a comment arrives. This has got the side effect that you can receive posts that had been started by a non-follower but had been commented by someone you follow. This setting deactivates this behaviour. When activated, you strictly only will receive posts from people you really do follow.'] = 'Wenn neue Kommentare empfangen werden führt das System eine Vervollständigung der Unterhaltung durch. Die hat den Nebeneffekt, dass Unterhaltungen in denen einer deiner Kontakte kommentiert haben, die aber nicht von einem deiner Kontakte begonnen wurden in deinem Netzwerk-Stream angezeigt werden können. Diese Option unterbindet dieses Verhalten. Ist sie aktiviert, wirst du ausschließlich die Unterhaltungen angezeigt bekommen, die von deinen Kontakten begonnen wurden.';
$a->strings['Followed content scope'] = 'Umfang zu folgender Inhalte';
$a->strings['By default, conversations in which your follows participated but didn\'t start will be shown in your timeline. You can turn this behavior off, or expand it to the conversations in which your follows liked a post.'] = 'Normalerweise werden Unterhaltungen an denen deine Kontakte beteiligt sind, sie aber nicht begonnen haben, in deiner Timeline angezeigt. Mit dieser Einstellung kann dieses Vorgehen kontrolliert werden. Es kann entweder dahin erweitert werden, dass auch Unterhaltungen angezeigt werden in denen deine Kontakte einen Kommentar mögen, oder komplett ausgeschaltet werden, so dass nur noch die Unterhaltungen angezeigt werden, die von deinen Kontakten gestartet wurden.';
$a->strings['Only conversations my follows started'] = 'Nur Unterhaltungen, die meine Kontakte gestartet haben';
$a->strings['Conversations my follows started or commented on (default)'] = 'Unterhaltungen an denen meine Kontakte beteiligt sind (Grundeinstellung)';
$a->strings['Any conversation my follows interacted with, including likes'] = 'Unterhaltungen mit denen meine Kontakte interagiert haben, inklusive likes';
$a->strings['Enable Content Warning'] = 'Inhaltswarnungen einschalten';
$a->strings['Users on networks like Mastodon or Pleroma are able to set a content warning field which collapse their post by default. This enables the automatic collapsing instead of setting the content warning as the post title. Doesn\'t affect any other content filtering you eventually set up.'] = 'Benutzer in Netzwerken wie Mastodon oder Pleroma können eine Warnung für sensitive Inhalte ihrer Beiträge erstellen. Mit dieser Option werden derart markierte Beiträge automatisch zusammengeklappt und die Inhaltswarnung wird als Titel des Beitrags angezeigt. Diese Option hat keinen Einfluss auf andere Inhaltsfilterungen, die du eventuell eingerichtet hast.';
$a->strings['Enable intelligent shortening'] = 'Intelligentes kürzen einschalten';
@ -1534,7 +1535,9 @@ $a->strings['Enable built-in Diaspora network compatibility for communicating wi
$a->strings['Verify SSL'] = 'SSL Überprüfen';
$a->strings['If you wish, you can turn on strict certificate checking. This will mean you cannot connect (at all) to self-signed SSL sites.'] = 'Wenn gewollt, kann man hier eine strenge Zertifikatskontrolle einstellen. Das bedeutet, dass man zu keinen Seiten mit selbst unterzeichnetem SSL-Zertifikat eine Verbindung herstellen kann.';
$a->strings['Proxy user'] = 'Proxy-Nutzer';
$a->strings['User name for the proxy server.'] = 'Nutzername für den Proxy-Server';
$a->strings['Proxy URL'] = 'Proxy-URL';
$a->strings['If you want to use a proxy server that Friendica should use to connect to the network, put the URL of the proxy here.'] = 'Wenn Friendica einen Proxy-Server verwenden soll um das Netzwerk zu erreichen, füge hier die URL des Proxys ein.';
$a->strings['Network timeout'] = 'Netzwerk-Wartezeit';
$a->strings['Value is in seconds. Set to 0 for unlimited (not recommended).'] = 'Der Wert ist in Sekunden. Setze 0 für unbegrenzt (nicht empfohlen).';
$a->strings['Maximum Load Average'] = 'Maximum Load Average';
@ -2586,8 +2589,6 @@ $a->strings['Unshare'] = 'Nicht mehr teilen';
$a->strings['%s (Received %s)'] = '%s (Empfangen %s)';
$a->strings['Comment this item on your system'] = 'Kommentiere diesen Beitrag von deinem System aus';
$a->strings['Remote comment'] = 'Entfernter Kommentar';
$a->strings['Pushed'] = 'Pushed';
$a->strings['Pulled'] = 'Pulled';
$a->strings['to'] = 'zu';
$a->strings['via'] = 'via';
$a->strings['Wall-to-Wall'] = 'Wall-to-Wall';

View file

@ -47,9 +47,9 @@
</div>
<div class="wall-item-tools" id="wall-item-tools-{{$item.id}}">
<div class="wall-item-delete-wrapper" id="wall-item-delete-wrapper-{{$item.id}}">
{{if $item.drop.dropping}}<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon drophide" title="{{$item.drop.delete}}" onmouseover="imgbright(this);" onmouseout="imgdull(this);"></a>{{/if}}
{{if $item.drop && $item.drop.dropping}}<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon drophide" title="{{$item.drop.delete}}" onmouseover="imgbright(this);" onmouseout="imgdull(this);"></a>{{/if}}
</div>
{{if $item.drop.pagedrop}}<input type="checkbox" onclick="checkboxhighlight(this);" title="{{$item.drop.select}}" class="item-select" name="itemselected[]" value="{{$item.id}}" />{{/if}}
{{if $item.drop && $item.drop.pagedrop}}<input type="checkbox" onclick="checkboxhighlight(this);" title="{{$item.drop.select}}" class="item-select" name="itemselected[]" value="{{$item.id}}" />{{/if}}
<div class="wall-item-delete-end"></div>
</div>
</div>

View file

@ -11,7 +11,7 @@
<div id="settings_general_expanded" class="settings-block" style="display: none;">
{{include file="field_checkbox.tpl" field=$accept_only_sharer}}
{{include file="field_select.tpl" field=$accept_only_sharer}}
{{include file="field_checkbox.tpl" field=$enable_cw}}
{{include file="field_checkbox.tpl" field=$enable_smart_shortening}}
{{include file="field_checkbox.tpl" field=$simple_shortening}}

View file

@ -39,28 +39,28 @@
<h2 class="settings-heading"><a href="javascript:;">{{$h_prv}}</a></h2>
<div class="settings-content-block">
<input type="hidden" name="visibility" value="{{$visibility}}"/>
{{include file="field_input.tpl" field=$maxreq}}
{{$profile_in_dir nofilter}}
{{include file="field_checkbox.tpl" field=$profile_in_net_dir}}
{{include file="field_checkbox.tpl" field=$hide_friends}}
{{if not $is_community}}{{include file="field_checkbox.tpl" field=$hide_friends}}{{/if}}
{{include file="field_checkbox.tpl" field=$hide_wall}}
{{include file="field_checkbox.tpl" field=$unlisted}}
{{if not $is_community}}{{include file="field_checkbox.tpl" field=$unlisted}}{{/if}}
{{include file="field_checkbox.tpl" field=$accessiblephotos}}
{{if not $is_community}}
{{include file="field_checkbox.tpl" field=$blockwall}}
{{include file="field_checkbox.tpl" field=$blocktags}}
{{/if}}
{{include file="field_checkbox.tpl" field=$unkmail}}
{{include file="field_input.tpl" field=$cntunkmail}}
{{$group_select nofilter}}
{{if not $is_community}}
<h3>{{$permissions}}</h3>
{{$aclselect nofilter}}
{{/if}}
<div class="settings-submit-wrapper">
<input type="submit" name="submit" class="settings-submit" value="{{$submit}}"/>
</div>

View file

@ -121,9 +121,9 @@
</div>
{{/if}}
<div class="wall-item-delete-wrapper" id="wall-item-delete-wrapper-{{$item.id}}">
{{if $item.drop.dropping}}<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon drophide" title="{{$item.drop.delete}}" onmouseover="imgbright(this);" onmouseout="imgdull(this);"></a>{{/if}}
{{if $item.drop && $item.drop.dropping}}<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon drophide" title="{{$item.drop.delete}}" onmouseover="imgbright(this);" onmouseout="imgdull(this);"></a>{{/if}}
</div>
{{if $item.drop.pagedrop}}<input type="checkbox" onclick="checkboxhighlight(this);" title="{{$item.drop.select}}" class="item-select" name="itemselected[]" value="{{$item.id}}" />{{/if}}
{{if $item.drop && $item.drop.pagedrop}}<input type="checkbox" onclick="checkboxhighlight(this);" title="{{$item.drop.select}}" class="item-select" name="itemselected[]" value="{{$item.id}}" />{{/if}}
<div class="wall-item-delete-end"></div>
</div>
</div>

View file

@ -1,6 +1,6 @@
<!-- TODO => Unknow block -->
<div class="wall-item-decor" style="display:none;">
<span class="icon s22 star {{$item.isstarred}}" id="starred-{{$item.id}}" title="{{$item.star.starred}}">{{$item.star.starred}}</span>
{{if $item.star}}<span class="icon s22 star {{$item.isstarred}}" id="starred-{{$item.id}}" title="{{$item.star.starred}}">{{$item.star.starred}}</span>{{/if}}
{{if $item.lock}}<span class="navicon lock fakelink" onclick="lockview(event, 'item', {{$item.id}});" title="{{$item.lock}}"></span><span class="fa fa-lock" aria-hidden="true"></span>{{/if}}
</div>
<!-- ./TODO => Unknow block -->
@ -245,7 +245,7 @@
</li>
{{/if}}
{{if $item.drop.dropping}}
{{if $item.drop && $item.drop.dropping}}
<li role="menuitem">
<a class="btn-link navicon delete" href="javascript:dropItem('item/drop/{{$item.id}}/{{$item.return}}', 'item-{{$item.guid}}');" title="{{$item.drop.delete}}"><i class="fa fa-trash" aria-hidden="true"></i> {{$item.drop.delete}}</a>
</li>
@ -270,7 +270,7 @@
{{/if}}
<span class="pull-right checkbox">
{{if $item.drop.pagedrop}}
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" title="{{$item.drop.select}}" name="itemselected[]" id="checkbox-{{$item.id}}" class="item-select" value="{{$item.id}}" />
<label for="checkbox-{{$item.id}}"></label>
{{/if}}

View file

@ -18,7 +18,7 @@
</div>
<div id="content-settings-content" class="panel-collapse collapse" role="tabpanel" aria-labelledby="content-settings-title">
<div class="panel-body">
{{include file="field_checkbox.tpl" field=$accept_only_sharer}}
{{include file="field_select.tpl" field=$accept_only_sharer}}
{{include file="field_checkbox.tpl" field=$enable_cw}}

View file

@ -70,28 +70,29 @@
</div>
<div id="privacy-settings-collapse" class="panel-collapse collapse" role="tabpanel" aria-labelledby="privacy-settings">
<div class="panel-body">
<input type="hidden" name="visibility" value="{{$visibility}}" />
{{include file="field_input.tpl" field=$maxreq}}
{{$profile_in_dir nofilter}}
{{include file="field_checkbox.tpl" field=$profile_in_net_dir}}
{{include file="field_checkbox.tpl" field=$hide_friends}}
{{if not $is_community}}{{include file="field_checkbox.tpl" field=$hide_friends}}{{/if}}
{{include file="field_checkbox.tpl" field=$hide_wall}}
{{include file="field_checkbox.tpl" field=$unlisted}}
{{if not $is_community}}{{include file="field_checkbox.tpl" field=$unlisted}}{{/if}}
{{include file="field_checkbox.tpl" field=$accessiblephotos}}
{{if not $is_community}}
{{include file="field_checkbox.tpl" field=$blockwall}}
{{include file="field_checkbox.tpl" field=$blocktags}}
{{/if}}
{{include file="field_checkbox.tpl" field=$unkmail}}
{{include file="field_input.tpl" field=$cntunkmail}}
{{$group_select nofilter}}
{{if not $is_community}}
<h3>{{$permissions}}</h3>
{{$aclselect nofilter}}
{{/if}}
</div>
<div class="panel-footer">
<button type="submit" name="submit" class="btn btn-primary" value="{{$submit}}">{{$submit}}</button>

View file

@ -336,7 +336,7 @@ as the value of $top_child_total (this is done at the end of this file)
{{/if}}
{{* Put additional actions in a dropdown menu *}}
{{if $item.menu && ($item.edpost || $item.tagger || $item.filer || $item.pin || $item.star || $item.follow_thread || $item.ignore || $item.drop.dropping)}}
{{if $item.menu && ($item.edpost || $item.tagger || $item.filer || $item.pin || $item.star || $item.follow_thread || $item.ignore || ($item.drop && $item.drop.dropping))}}
<span role="presentation" class="separator"></span>
<span class="more-links btn-group{{if $item.thread_level > 1}} dropup{{/if}}">
<button type="button" class="btn-link dropdown-toggle" data-toggle="dropdown" id="dropdownMenuOptions-{{$item.id}}" aria-haspopup="true" aria-expanded="false" title="{{$item.menu}}"><i class="fa fa-ellipsis-h" aria-hidden="true"></i>&nbsp;{{$item.menu}}</button>
@ -385,7 +385,7 @@ as the value of $top_child_total (this is done at the end of this file)
</li>
{{/if}}
{{if ($item.edpost || $item.tagger || $item.filer || $item.pin || $item.star || $item.follow_thread) && ($item.ignore || $item.drop.dropping)}}
{{if ($item.edpost || $item.tagger || $item.filer || $item.pin || $item.star || $item.follow_thread) && ($item.ignore || ($item.drop && $item.drop.dropping))}}
<li role="separator" class="divider"></li>
{{/if}}
@ -398,7 +398,7 @@ as the value of $top_child_total (this is done at the end of this file)
</li>
{{/if}}
{{if $item.drop.dropping}}
{{if $item.drop && $item.drop.dropping}}
<li role="menuitem">
<a class="btn-link navicon delete" href="javascript:dropItem('item/drop/{{$item.id}}/{{$item.return}}', 'item-{{$item.guid}}');" title="{{$item.drop.delete}}"><i class="fa fa-trash" aria-hidden="true"></i> {{$item.drop.delete}}</a>
</li>
@ -429,7 +429,7 @@ as the value of $top_child_total (this is done at the end of this file)
{{/if}}
<span class="pull-right checkbox">
{{if $item.drop.pagedrop}}
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" title="{{$item.drop.select}}" name="itemselected[]" id="checkbox-{{$item.id}}" class="item-select" value="{{$item.id}}" />
<label for="checkbox-{{$item.id}}"></label>
{{/if}}
@ -506,7 +506,7 @@ as the value of $top_child_total (this is done at the end of this file)
</div>
{{/if}}
{{if $item.edpost || $item.tagger || $item.filer || $item.pin || $item.star || $item.follow_thread || $item.ignore || $item.drop.dropping}}
{{if $item.edpost || $item.tagger || $item.filer || $item.pin || $item.star || $item.follow_thread || $item.ignore || ($item.drop && $item.drop.dropping)}}
<div class="more-links btn-group{{if $item.thread_level > 1}} dropup{{/if}}">
<button type="button" class="btn btn-sm dropdown-toggle" data-toggle="dropdown" id="dropdownMenuOptions-{{$item.id}}" aria-haspopup="true" aria-expanded="false" title="{{$item.menu}}"><i class="fa fa-ellipsis-h" aria-hidden="true"></i></button>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="dropdownMenuOptions-{{$item.id}}">
@ -548,7 +548,7 @@ as the value of $top_child_total (this is done at the end of this file)
</li>
{{/if}}
{{if $item.ignore || $item.drop.dropping}}
{{if $item.ignore || ($item.drop && $item.drop.dropping)}}
<li role="separator" class="divider"></li>
{{/if}}
@ -561,7 +561,7 @@ as the value of $top_child_total (this is done at the end of this file)
</li>
{{/if}}
{{if $item.drop.dropping}}
{{if $item.drop && $item.drop.dropping}}
<li role="menuitem">
<a class="btn-link navicon delete" href="javascript:dropItem('item/drop/{{$item.id}}/{{$item.return}}', 'item-{{$item.guid}}');" title="{{$item.drop.delete}}"><i class="fa fa-trash" aria-hidden="true"></i> {{$item.drop.delete}}</a>
</li>
@ -571,7 +571,7 @@ as the value of $top_child_total (this is done at the end of this file)
</div>
{{/if}}
<span class="pull-right checkbox">
{{if $item.drop.pagedrop}}
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" title="{{$item.drop.select}}" name="itemselected[]" id="checkbox-{{$item.id}}" class="item-select" value="{{$item.id}}" />
<label for="checkbox-{{$item.id}}"></label>
{{/if}}

View file

@ -71,10 +71,10 @@
<div class="wall-item-actions-tools">
{{if $item.drop.pagedrop}}
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" title="{{$item.drop.select}}" name="itemselected[]" class="item-select" value="{{$item.id}}" />
{{/if}}
{{if $item.drop.dropping}}
{{if $item.drop && $item.drop.dropping}}
<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon delete s16" title="{{$item.drop.delete}}">{{$item.drop.delete}}</a>
{{/if}}
{{if $item.edpost}}

View file

@ -40,10 +40,10 @@
{{$item.ago}} {{$item.body_html nofilter}}
</div>
<div class="wall-item-tools">
{{if $item.drop.pagedrop}}
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" title="{{$item.drop.select}}" name="itemselected[]" class="item-select" value="{{$item.id}}" />
{{/if}}
{{if $item.drop.dropping}}
{{if $item.drop && $item.drop.dropping}}
<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon delete s16" title="{{$item.drop.delete}}">{{$item.drop.delete}}</a>
{{/if}}
</div>

View file

@ -145,10 +145,10 @@
<div class="wall-item-actions-tools">
{{if $item.drop.pagedrop}}
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" title="{{$item.drop.select}}" name="itemselected[]" class="item-select" value="{{$item.id}}" />
{{/if}}
{{if $item.drop.dropping}}
{{if $item.drop && $item.drop.dropping}}
<a href="item/drop/{{$item.id}}/{{$item.return}}" onclick="return confirmDelete();" class="icon delete s16" title="{{$item.drop.delete}}">{{$item.drop.delete}}</a>
{{/if}}
{{if $item.edpost}}

View file

@ -26,9 +26,9 @@
</div>
<div class="wall-item-tools" id="wall-item-tools-{{$item.id}}">
<div class="wall-item-delete-wrapper" id="wall-item-delete-wrapper-{{$item.id}}">
{{if $item.drop.dropping}}<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon drophide" title="{{$item.drop.delete}}" onmouseover="imgbright(this);" onmouseout="imgdull(this);"></a>{{/if}}
{{if $item.drop && $item.drop.dropping}}<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon drophide" title="{{$item.drop.delete}}" onmouseover="imgbright(this);" onmouseout="imgdull(this);"></a>{{/if}}
</div>
{{if $item.drop.pagedrop}}<input type="checkbox" onclick="checkboxhighlight(this);" title="{{$item.drop.select}}" class="item-select" name="itemselected[]" value="{{$item.id}}" />{{/if}}
{{if $item.drop && $item.drop.pagedrop}}<input type="checkbox" onclick="checkboxhighlight(this);" title="{{$item.drop.select}}" class="item-select" name="itemselected[]" value="{{$item.id}}" />{{/if}}
<div class="wall-item-delete-end"></div>
</div>
<div class="wall-item-content" id="wall-item-content-{{$item.id}}">

View file

@ -135,12 +135,12 @@
{{/if}}
<div class="wall-item-delete-wrapper" id="wall-item-delete-wrapper-{{$item.id}}">
{{if $item.drop.dropping}}
{{if $item.drop && $item.drop.dropping}}
<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon drophide" title="{{$item.drop.delete}}" onmouseover="imgbright(this);" onmouseout="imgdull(this);"></a>
{{/if}}
</div>
{{if $item.drop.pagedrop}}
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" onclick="checkboxhighlight(this);" title="{{$item.drop.select}}" class="item-select" name="itemselected[]" value="{{$item.id}}" />
{{/if}}

View file

@ -75,10 +75,10 @@
<div class="wall-item-actions-tools">
{{if $item.drop.pagedrop}}
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" title="{{$item.drop.select}}" name="itemselected[]" class="item-select" value="{{$item.id}}" />
{{/if}}
{{if $item.drop.dropping}}
{{if $item.drop && $item.drop.dropping}}
<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon delete s16" title="{{$item.drop.delete}}">{{$item.drop.delete}}</a>
{{/if}}
{{if $item.edpost}}

View file

@ -40,10 +40,10 @@
{{$item.ago}} {{$item.body_html nofilter}}
</div>
<div class="wall-item-tools">
{{if $item.drop.pagedrop}}
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" title="{{$item.drop.select}}" name="itemselected[]" class="item-select" value="{{$item.id}}" />
{{/if}}
{{if $item.drop.dropping}}
{{if $item.drop && $item.drop.dropping}}
<a href="item/drop/{{$item.id}}" onclick="return confirmDelete();" class="icon delete s16" title="{{$item.drop.delete}}">{{$item.drop.delete}}</a>
{{/if}}
</div>

View file

@ -162,10 +162,10 @@
<div class="wall-item-actions-tools">
{{if $item.drop.pagedrop}}
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" title="{{$item.drop.select}}" name="itemselected[]" class="item-select" value="{{$item.id}}" />
{{/if}}
{{if $item.drop.dropping}}
{{if $item.drop && $item.drop.dropping}}
<a role="button" href="item/drop/{{$item.id}}/{{$item.return}}" onclick="return confirmDelete();" title="{{$item.drop.delete}}"><i class="icon-trash icon-large"><span class="sr-only">{{$item.drop.delete}}</span></i></a>
{{/if}}
{{if $item.edpost}}