Merge remote-tracking branch 'friendica/stable' into develop

# Conflicts:
#	composer.lock
This commit is contained in:
Hypolite Petovan 2020-09-20 15:32:58 -04:00
commit a852455d0e
154 changed files with 26577 additions and 25630 deletions

4
.gitignore vendored
View file

@ -71,8 +71,8 @@ venv/
/addons /addons
/addon /addon
#ignore .htaccess #ignore base .htaccess
.htaccess /.htaccess
#ignore filesystem storage default path #ignore filesystem storage default path
/storage /storage

View file

@ -1,3 +1,6 @@
# This file is meant to be copied to ".htaccess" on Apache-powered web servers.
# The created .htaccess file can be edited manually and will not be overwritten by Friendica updates.
Options -Indexes Options -Indexes
AddType application/x-java-archive .jar AddType application/x-java-archive .jar
AddType audio/ogg .oga AddType audio/ogg .oga

View file

@ -1,4 +1,78 @@
Version 2020.09 (unreleased) Version 2020.09 (2020-09-20)
Friendica Core:
Updates to the translations: DE, EN GB, EN US, ES, FR, IT, NL, PL, RU, ZH_CN [translation teams]
Updates to the themes (all) [MrPetovan, tobiasd]
Updates to the documentation [annando, mpanhans, realkinetix, tobiasd]
General code cleanup and refactoring [annando, MrPetovan, nupplaphil]
Enhanced the API [annando]
Enhanced the processing of background jobs [annando]
Enhanced federation of activities [annando, vpzomtrrfrt]
Enhanced the user notifications[annando]
Enhanced database usage [annando, MrPetovan]
Enhanced ActivityPub support for forums [annando]
Enhanced the utilization of the cache [annando, MrPetovan]
Enhanced the performance of the daemon [annando]
Enhanced the communication with the directory servers [annando]
Enhanced the re-sharing of items [annando]
Enhanced sample lighttpd and nginx configs [MrPetovan, tobiasd]
Enhanced the checks for incoming postings using ActivityPub [annando, Roger Meyer]
Enhanced the import of RSS feeds by removing tracking pixels [annando]
Enhanced the speed of the full text search [annando]
Replaced library used for text completion [MrPetovan]
Fixed a problem that prevented recipients of direct messages to be selected [MrPetovan]
Fixed a problem that prevented new email contacts from being added [annando]
Fixed a problem with the console command search [tobiasd]
Fixed a problem during the search for contacts [annando]
Fixed a problem with the JOT of private notes [MrPetovan]
Fixed missing HTML encoding [MrPetovan]
Fixed a layout problem with the frio composer for new postings [MrPetovan]
Fixed some composer notices [nupplaphil]
Fixed a problem for empty preview data when importing feed posts [annando]
Fixed a problem with the pager on search result pages [annando]
Fixed some templates to show the correct un-/follow button for contacts [annando]
Fixed a problem with the generation of the Message-ID of notification emails [nupplaphil]
Added nodeinfo2 support [annando]
Added CSV export and import of blocked servers to the console [tobiasd]
Added new admin debug module for ActivityPub [MrPetovan]
Added the automatic determination of frequency to pull feeds [annando]
Added signed fetching from system users for ActivityPub [annando]
Added the discovery of new peers from contacts [annando]
Added the directory API endpoint [annando]
Added support for signed outbox requests [annando]
Added direction functionality for clarification of posting flow [annando]
Added the ability to set the database version [annando]
Added support for ActivityPub relay server [annando]
By default display of re-sharer information is now flattened [annando]
Removed some unused POCO functionality [annando]
Removed the unused rating functionality [annando]
Removed unneeded network request for local stuff [annando]
Removed some useless info messages [annando]
Reworked some additional features according to a user voting [MrPetovan]
Friendica Addons:
Updates to the translations: DE, EN GB, EN US, IT, NL, RU, ZH_CN [translation teams]
Updates to the docs [SpencerDub]
General code cleanup and maintenance [annando, MrPetovan]
blockbot:
added some "good" bots [annando]
forumdirectory:
fixed some SQL queries [MrPetovan]
phpmailer:
fixed a problem leading to double message ID headers [nupplaphil]
qcomment:
restructured the addon and fixed a bug preventing the addon from working [MrPetovan]
Closed Issues:
2811, 4606, 5742, 5782, 7660, 8676, 8788, 8797, 8798, 8847, 8860,
8874, 8882, 8885, 8906, 8914, 8922, 8928, 8929, 8935, 8940, 8941,
8956, 8958, 8961, 8967, 8989, 8993, 8994, 8995, 8997, 8999, 9000,
9004, 9013, 9015, 9051, 9064, 9065, 9072, 9081, 9090, 9091, 9099,
9107, 9135, 9136, 9137, 9138, 9140, 9142, 9150, 9153, 9154, 9163,
9164, 9172, 9182, 9192, 9193, 9204, 9210, 9229, 9231, 9246
Version 2020.07-1 (2020-09-08)
Friendica Core
Fixed a problem that leaked sensitive information [Roger Meyer, MrPetovan]
Version 2020.07 (2020-07-12) Version 2020.07 (2020-07-12)
Friendica Core: Friendica Core:
@ -668,7 +742,7 @@ Version 2018.09 (2018-09-23)
Version 2018.05 (2018-06-01) Version 2018.05 (2018-06-01)
Friendica Core: Friendica Core:
Update to the translations (DE, EN-GB, EN-US, FI, IS, IT, NL, PL, RU, ZN CH) [translation teams] Update to the translations (DE, EN-GB, EN-US, FI, IS, IT, NL, PL, RU, ZN CH) [translation teams]
Update to the documentation [andyhee, annando, fabrixxm, M-arcus, MrPedovan, rudloff, tobiasd] Update to the documentation [andyhee, annando, fabrixxm, M-arcus, MrPetovan, rudloff, tobiasd]
Enhancements to the DB handling [annando] Enhancements to the DB handling [annando]
Enhancements to the relay system [annando] Enhancements to the relay system [annando]
Enhancements to the handling of URL that contain unicode characters [annando] Enhancements to the handling of URL that contain unicode characters [annando]

View file

@ -55,6 +55,7 @@ Chris Case
Christian González Christian González
Christian M. Grube Christian M. Grube
Christian Vogeley Christian Vogeley
Christian Wiwie
Cohan Robinson Cohan Robinson
Copiis Praeesse Copiis Praeesse
CrystalStiletto CrystalStiletto
@ -114,7 +115,6 @@ Hypolite Petovan
Ilmari Ilmari
ImgBotApp ImgBotApp
irhen irhen
Jak
Jakob Jakob
Jens Tautenhahn Jens Tautenhahn
jensp jensp
@ -122,6 +122,7 @@ Jeroen De Meerleer
jeroenpraat jeroenpraat
Joan Bar Joan Bar
JOduMonT JOduMonT
joe slam
Johannes Schwab Johannes Schwab
John Brazil John Brazil
Jonatan Nyberg Jonatan Nyberg
@ -143,7 +144,6 @@ Leberwurscht
Leonard Lausen Leonard Lausen
Lionel Triay Lionel Triay
loma-one loma-one
loma1
Lorem Ipsum Lorem Ipsum
Ludovic Grossard Ludovic Grossard
Lynn Stephenson Lynn Stephenson
@ -173,6 +173,7 @@ Michal Šupler
Michalina Michalina
Mike Macgirvin Mike Macgirvin
miqrogroove miqrogroove
mpanhans
mytbk mytbk
nathilia-peirce nathilia-peirce
Nicola Spanti Nicola Spanti
@ -212,6 +213,7 @@ repat
Ricardo Pereira Ricardo Pereira
Rik 4 Rik 4
RJ Madsen RJ Madsen
Roger Meyer
Roland Häder Roland Häder
Rui Andrada Rui Andrada
rwa rwa
@ -230,7 +232,7 @@ Simon L'nu
Simó Albert i Beltran Simó Albert i Beltran
softmetz softmetz
soko1 soko1
SpencerDub Spencer Dub
St John Karp St John Karp
Stanislav N. Stanislav N.
Steffen K9 Steffen K9
@ -268,7 +270,6 @@ U-SOUND\mike
ufic ufic
Ulf Rompe Ulf Rompe
Unknown Unknown
Valvin
Valvin A Valvin A
Vasudev Kamath Vasudev Kamath
Vasya Novikov Vasya Novikov

View file

@ -1 +1 @@
2020.09-dev 2020.09

10
bin/.htaccess Normal file
View file

@ -0,0 +1,10 @@
# This file prevents browser access to Friendica command-line scripts on Apache-powered web servers.
# It isn't meant to be edited manually, please check the base Friendica folder for the .htaccess-dist file instead.
<IfModule authz_host_module>
Require all denied
</IfModule>
<IfModule !authz_host_module>
Order Allow,Deny
Deny from all
</IfModule>

View file

@ -51,6 +51,11 @@
* *
*/ */
if (php_sapi_name() !== 'cli') {
header($_SERVER["SERVER_PROTOCOL"] . ' 403 Forbidden');
exit();
}
use Dice\Dice; use Dice\Dice;
use Friendica\App\Mode; use Friendica\App\Mode;
use Friendica\Util\ExAuth; use Friendica\Util\ExAuth;

View file

@ -20,6 +20,11 @@
* *
*/ */
if (php_sapi_name() !== 'cli') {
header($_SERVER["SERVER_PROTOCOL"] . ' 403 Forbidden');
exit();
}
use Dice\Dice; use Dice\Dice;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;

View file

@ -23,6 +23,11 @@
* This script was taken from http://php.net/manual/en/function.pcntl-fork.php * This script was taken from http://php.net/manual/en/function.pcntl-fork.php
*/ */
if (php_sapi_name() !== 'cli') {
header($_SERVER["SERVER_PROTOCOL"] . ' 403 Forbidden');
exit();
}
use Dice\Dice; use Dice\Dice;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\Worker; use Friendica\Core\Worker;

View file

@ -34,7 +34,7 @@ dontinclude = ['root', 'friendica', 'bavatar', 'tony baldwin', 'Taek', 'silke m'
path = os.path.abspath(argv[0].split('bin/dev/make_credits.py')[0]) path = os.path.abspath(argv[0].split('bin/dev/make_credits.py')[0])
print('> base directory is assumed to be: '+path) print('> base directory is assumed to be: '+path)
# a place to store contributors # a place to store contributors
contributors = ["Andi Stadler", "Ratten", "Vít Šesták 'v6ak'"] contributors = ["Andi Stadler", "Ratten", "Roger Meyer", "Vít Šesták 'v6ak'"]
# get the contributors # get the contributors
print('> getting contributors to the friendica core repository') print('> getting contributors to the friendica core repository')
p = subprocess.Popen(['git', 'shortlog', '--no-merges', '-s'], p = subprocess.Popen(['git', 'shortlog', '--no-merges', '-s'],

View file

@ -26,6 +26,10 @@
* *
*/ */
if (php_sapi_name() !== 'cli') {
header($_SERVER["SERVER_PROTOCOL"] . ' 403 Forbidden');
exit();
}
if (($_SERVER["argc"] > 1) && isset($_SERVER["argv"][1])) { if (($_SERVER["argc"] > 1) && isset($_SERVER["argv"][1])) {
echo $_SERVER["argv"][1]; echo $_SERVER["argv"][1];

View file

@ -24,6 +24,11 @@
* Usage: php bin/wait-for-connection {HOST} {PORT} [{TIMEOUT}] * Usage: php bin/wait-for-connection {HOST} {PORT} [{TIMEOUT}]
*/ */
if (php_sapi_name() !== 'cli') {
header($_SERVER["SERVER_PROTOCOL"] . ' 403 Forbidden');
exit();
}
$timeout = 60; $timeout = 60;
switch ($argc) { switch ($argc) {
case 4: case 4:

View file

@ -21,6 +21,11 @@
* Starts the background processing * Starts the background processing
*/ */
if (php_sapi_name() !== 'cli') {
header($_SERVER["SERVER_PROTOCOL"] . ' 403 Forbidden');
exit();
}
use Dice\Dice; use Dice\Dice;
use Friendica\App; use Friendica\App;
use Friendica\Core\Process; use Friendica\Core\Process;

View file

@ -38,7 +38,7 @@ use Friendica\Util\DateTimeFormat;
define('FRIENDICA_PLATFORM', 'Friendica'); define('FRIENDICA_PLATFORM', 'Friendica');
define('FRIENDICA_CODENAME', 'Red Hot Poker'); define('FRIENDICA_CODENAME', 'Red Hot Poker');
define('FRIENDICA_VERSION', '2020.09-dev'); define('FRIENDICA_VERSION', '2020.09');
define('DFRN_PROTOCOL_VERSION', '2.23'); define('DFRN_PROTOCOL_VERSION', '2.23');
define('NEW_UPDATE_ROUTINE_VERSION', 1170); define('NEW_UPDATE_ROUTINE_VERSION', 1170);
@ -382,38 +382,6 @@ function is_site_admin()
return local_user() && $admin_email && in_array($a->user['email'] ?? '', $adminlist); return local_user() && $admin_email && in_array($a->user['email'] ?? '', $adminlist);
} }
function explode_querystring($query)
{
$arg_st = strpos($query, '?');
if ($arg_st !== false) {
$base = substr($query, 0, $arg_st);
$arg_st += 1;
} else {
$base = '';
$arg_st = 0;
}
$args = explode('&', substr($query, $arg_st));
foreach ($args as $k => $arg) {
/// @TODO really compare type-safe here?
if ($arg === '') {
unset($args[$k]);
}
}
$args = array_values($args);
if (!$base) {
$base = $args[0];
unset($args[0]);
$args = array_values($args);
}
return [
'base' => $base,
'args' => $args,
];
}
/** /**
* Returns the complete URL of the current page, e.g.: http(s)://something.com/network * Returns the complete URL of the current page, e.g.: http(s)://something.com/network
* *

View file

@ -129,7 +129,7 @@
"mikey179/vfsstream": "^1.6", "mikey179/vfsstream": "^1.6",
"mockery/mockery": "^1.2", "mockery/mockery": "^1.2",
"johnkary/phpunit-speedtrap": "1.1", "johnkary/phpunit-speedtrap": "1.1",
"jakub-onderka/php-parallel-lint": "^1.0" "php-parallel-lint/php-parallel-lint": "^1.2"
}, },
"scripts": { "scripts": {
"test": "phpunit" "test": "phpunit"

104
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ffe94190e166cebf80601fc3d6d26be0", "content-hash": "ed9aa898eaf8a1f8a807f3be9eecc3d7",
"packages": [ "packages": [
{ {
"name": "asika/simple-console", "name": "asika/simple-console",
@ -3487,55 +3487,6 @@
], ],
"time": "2016-01-20T08:20:44+00:00" "time": "2016-01-20T08:20:44+00:00"
}, },
{
"name": "jakub-onderka/php-parallel-lint",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/JakubOnderka/PHP-Parallel-Lint.git",
"reference": "04fbd3f5fb1c83f08724aa58a23db90bd9086ee8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/JakubOnderka/PHP-Parallel-Lint/zipball/04fbd3f5fb1c83f08724aa58a23db90bd9086ee8",
"reference": "04fbd3f5fb1c83f08724aa58a23db90bd9086ee8",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"jakub-onderka/php-console-highlighter": "~0.3",
"nette/tester": "~1.3",
"squizlabs/php_codesniffer": "~2.7"
},
"suggest": {
"jakub-onderka/php-console-highlighter": "Highlight syntax in code snippet"
},
"bin": [
"parallel-lint"
],
"type": "library",
"autoload": {
"classmap": [
"./"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Jakub Onderka",
"email": "ahoj@jakubonderka.cz"
}
],
"description": "This tool check syntax of PHP files about 20x faster than serial check.",
"homepage": "https://github.com/JakubOnderka/PHP-Parallel-Lint",
"abandoned": "php-parallel-lint/php-parallel-lint",
"time": "2018-02-24T15:31:20+00:00"
},
{ {
"name": "johnkary/phpunit-speedtrap", "name": "johnkary/phpunit-speedtrap",
"version": "v1.1.0", "version": "v1.1.0",
@ -3740,6 +3691,59 @@
], ],
"time": "2017-10-19T19:58:43+00:00" "time": "2017-10-19T19:58:43+00:00"
}, },
{
"name": "php-parallel-lint/php-parallel-lint",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git",
"reference": "474f18bc6cc6aca61ca40bfab55139de614e51ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/474f18bc6cc6aca61ca40bfab55139de614e51ca",
"reference": "474f18bc6cc6aca61ca40bfab55139de614e51ca",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=5.4.0"
},
"replace": {
"grogy/php-parallel-lint": "*",
"jakub-onderka/php-parallel-lint": "*"
},
"require-dev": {
"nette/tester": "^1.3 || ^2.0",
"php-parallel-lint/php-console-highlighter": "~0.3",
"squizlabs/php_codesniffer": "~3.0"
},
"suggest": {
"php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet"
},
"bin": [
"parallel-lint"
],
"type": "library",
"autoload": {
"classmap": [
"./"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Jakub Onderka",
"email": "ahoj@jakubonderka.cz"
}
],
"description": "This tool check syntax of PHP files about 20x faster than serial check.",
"homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint",
"time": "2020-04-04T12:18:32+00:00"
},
{ {
"name": "phpdocumentor/reflection-common", "name": "phpdocumentor/reflection-common",
"version": "1.0.1", "version": "1.0.1",

View file

@ -1,6 +1,6 @@
-- ------------------------------------------ -- ------------------------------------------
-- Friendica 2020.09-dev (Red Hot Poker) -- Friendica 2020.09-rc (Red Hot Poker)
-- DB_UPDATE_VERSION 1367 -- DB_UPDATE_VERSION 1368
-- ------------------------------------------ -- ------------------------------------------
@ -786,6 +786,7 @@ CREATE TABLE IF NOT EXISTS `item-content` (
`verb` varchar(100) NOT NULL DEFAULT '' COMMENT 'ActivityStreams verb', `verb` varchar(100) NOT NULL DEFAULT '' COMMENT 'ActivityStreams verb',
PRIMARY KEY(`id`), PRIMARY KEY(`id`),
UNIQUE INDEX `uri-plink-hash` (`uri-plink-hash`), UNIQUE INDEX `uri-plink-hash` (`uri-plink-hash`),
FULLTEXT INDEX `title-content-warning-body` (`title`,`content-warning`,`body`),
INDEX `uri` (`uri`(191)), INDEX `uri` (`uri`(191)),
INDEX `plink` (`plink`(191)), INDEX `plink` (`plink`(191)),
INDEX `uri-id` (`uri-id`), INDEX `uri-id` (`uri-id`),

View file

@ -4,49 +4,46 @@ Forums
* [Home](help) * [Home](help)
Friendica also lets you create forums and/or celebrity accounts. Friendica also lets you create community forums and other types of accounts that can function as discussion forums, celebrity accounts, announcement channels, news reflectors, or organization pages, depending on how you want to interact with others. Management of these pages can be delegated to other accounts, or a parent account can be designated to easily toggle multiple identities.
Every page in Friendica has a nickname and these must all be unique. Every page in Friendica has a nickname and these must all be unique. This applies to all forums, whether they are normal profiles or forum profiles.
This applies to all forums, whether they are normal profiles or forum profiles.
Therefore the first thing you need to do to create a new forum is to register a new account for the forum. Managing Accounts
Please note that the site administrator can restrict and/or regulate the registration of new accounts.
If you create a second account on a system and use the same email address or OpenID account as an existing account, you will no longer be able to use the email address (or OpenID) to log in to the account.
You should log in using the account nickname instead.
On the new account, visit the 'Settings' page.
Towards the end of the page are "Advanced Account/Page Type Settings".
Typically you would use "Normal Account" for a normal personal account.
This is the default selection.
Community Forum/Celebrity Accounts provide the ability for people to become friends/fans of the forum without requiring approval.
The exact setting you would use depends on how you wish to interact with people who join the page.
The "Soapbox" setting lets the page owner control all communications.
Everything you post will go out to the forum members, but there will be no opportunity for interaction.
This setting would typically be used for announcements or corporate communications.
The most common setting is the "Community Forum".
This creates a forum page where all members can freely interact.
The "Automatic Friend Account" is typically used for personal profile forums where you wish to automatically approve any friendship/connection requests.
Managing Multiple forums
--- ---
We recommend that you create group forums with the same email address and password as your normal account. To create a new linked account that can be used as a forum, log in to your normal account and go to Settings > Manage Accounts.
If you do this, you will find a new "Manage" tab on the menu bar which lets you toggle identities easily and manage your forums. Here you can register additional accounts with new nicknames that will be linked to your primary account.
You are not required to do this, but the alternative is to log out and log back into the other account to manage alternate forums.
This could get cumbersome if you manage several different forums/identities.
You may also appoint a delegate to manage your forum. You may appoint a delegate to manage your new account (e.g. forum page).
Do this by visiting the [Delegation Setup Page](settings/delegation). The Delegates section of Manage Accounts page will provide you with a list of contacts on this instance under "Potential Delegates".
This will provide you with a list of contacts on this system under "Potential Delegates".
Selecting one or more persons will give them access to manage your forum. Selecting one or more persons will give them access to manage your forum.
They will be able to edit contacts, profiles, and all content for this account/page. They will be able to edit contacts, profiles, and all content for this account/page.
Please use this facility wisely. Please use this facility wisely.
Delegated managers will not be able to alter basic account settings such as passwords or page types and/or remove the account. Delegated managers will not be able to alter basic account settings, such as passwords or page types, or remove the account.
Additionally, this page is also where you can choose to designate an account as a parent user.
If your primary account is designated as the parent user, you will be able to easily toggle identities and manage your forums or other types of accounts.
Types of Accounts
---
On the new account, visit the Settings > Account page.
Towards the end of the page is a section for "Advanced account types".
Typically you would use "Personal Page - Standard" for a normal personal account with manual approval of “friends” and “followers.”
This is the default selection.
On this page you can change the type of account if desired.
The other subtypes of a Personal Page are “Soapbox” and “Love-all.”
A Soapbox account is an announcement channel that automatically approvals follower requests.
Everything posted by the account will go out to the followers, but there will be no opportunity for interaction.
This setting would typically be used for announcements or corporate communications.
“Love-all” automatically approves contacts as friends.
In addition to Personal Page, there are options for Organization Page, News Page, and Community Forum.
Organization and New Pages automatically approve contact requests as followers.
Community Forum provide the ability for people to become friends/fans of the forum without requiring approval.
This creates a forum page where all members can freely interact.
Posting to Community forums Posting to Community forums
--- ---

View file

@ -33,7 +33,7 @@ The account will expire after 7 days, but you can ask the server admin to keep y
* Apache with mod-rewrite enabled and "Options All" so you can use a local `.htaccess` file * Apache with mod-rewrite enabled and "Options All" so you can use a local `.htaccess` file
* PHP 7+ (PHP 7.1+ is recommended for performance and official support) * PHP 7+ (PHP 7.1+ is recommended for performance and official support)
* PHP *command line* access with register_argc_argv set to true in the php.ini file * PHP *command line* access with register_argc_argv set to true in the php.ini file
* Curl, GD, PDO, MySQLi, hash, xml, zip and OpenSSL extensions * Curl, GD, PDO, mbstrings, MySQLi, hash, xml, zip and OpenSSL extensions
* The POSIX module of PHP needs to be activated (e.g. [RHEL, CentOS](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) have disabled it) * The POSIX module of PHP needs to be activated (e.g. [RHEL, CentOS](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) have disabled it)
* some form of email server or email gateway such that PHP mail() works * some form of email server or email gateway such that PHP mail() works
* MySQL 5.6+ or an equivalent alternative for MySQL (MariaDB, Percona Server etc.) * MySQL 5.6+ or an equivalent alternative for MySQL (MariaDB, Percona Server etc.)
@ -47,7 +47,6 @@ For alternative server configurations (such as Nginx server and MariaDB database
### Optional ### Optional
* PHP ImageMagick extension (php-imagick) for animated GIF support. * PHP ImageMagick extension (php-imagick) for animated GIF support.
* [Composer](https://getcomposer.org/) for a git install
## Installation procedure ## Installation procedure
@ -61,6 +60,8 @@ If this is nothing for you, you might be interested in
### Get Friendica ### Get Friendica
Download the full archive of the stable release of Friendica core and the addons from [the project homepage](https://friendi.ca/resources/download-files/).
Make sure that the version of the Friendica archive and the addons match.
Unpack the Friendica files into the root of your web server document area. Unpack the Friendica files into the root of your web server document area.
If you copy the directory tree to your webserver, make sure that you also copy `.htaccess-dist` - as "dot" files are often hidden and aren't normally copied. If you copy the directory tree to your webserver, make sure that you also copy `.htaccess-dist` - as "dot" files are often hidden and aren't normally copied.

View file

@ -27,6 +27,7 @@ The console provides the following commands:
* typo: Checks for parse errors in Friendica files * typo: Checks for parse errors in Friendica files
* postupdate: Execute pending post update scripts (can last days) * postupdate: Execute pending post update scripts (can last days)
* storage: Manage storage backend * storage: Manage storage backend
* relay: Manage ActivityPub relay servers
Please consult *bin/console help* on the command line interface of your server for details about the commands. Please consult *bin/console help* on the command line interface of your server for details about the commands.

View file

@ -311,22 +311,22 @@ function api_call(App $a, App\Arguments $args = null)
} }
$type = "json"; $type = "json";
if (strpos($args->getQueryString(), ".xml") > 0) { if (strpos($args->getCommand(), ".xml") > 0) {
$type = "xml"; $type = "xml";
} }
if (strpos($args->getQueryString(), ".json") > 0) { if (strpos($args->getCommand(), ".json") > 0) {
$type = "json"; $type = "json";
} }
if (strpos($args->getQueryString(), ".rss") > 0) { if (strpos($args->getCommand(), ".rss") > 0) {
$type = "rss"; $type = "rss";
} }
if (strpos($args->getQueryString(), ".atom") > 0) { if (strpos($args->getCommand(), ".atom") > 0) {
$type = "atom"; $type = "atom";
} }
try { try {
foreach ($API as $p => $info) { foreach ($API as $p => $info) {
if (strpos($args->getQueryString(), $p) === 0) { if (strpos($args->getCommand(), $p) === 0) {
if (!api_check_method($info['method'])) { if (!api_check_method($info['method'])) {
throw new MethodNotAllowedException(); throw new MethodNotAllowedException();
} }

View file

@ -520,10 +520,6 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
$threadsid++; $threadsid++;
$owner_url = '';
$owner_name = '';
$sparkle = '';
// prevent private email from leaking. // prevent private email from leaking.
if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) { if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
continue; continue;
@ -540,14 +536,14 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
'network' => $item['author-network'], 'url' => $item['author-link']]; 'network' => $item['author-network'], 'url' => $item['author-link']];
$profile_link = Contact::magicLinkByContact($author); $profile_link = Contact::magicLinkByContact($author);
$sparkle = '';
if (strpos($profile_link, 'redir/') === 0) { if (strpos($profile_link, 'redir/') === 0) {
$sparkle = ' sparkle'; $sparkle = ' sparkle';
} }
$locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => '']; $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
Hook::callAll('render_location',$locate); Hook::callAll('render_location',$locate);
$location_html = $locate['html'] ?: Strings::escapeHtml($locate['location'] ?: $locate['coord'] ?: '');
$location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate));
localize_item($item); localize_item($item);
if ($mode === 'network-new') { if ($mode === 'network-new') {
@ -563,10 +559,6 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
'delete' => DI::l10n()->t('Delete'), 'delete' => DI::l10n()->t('Delete'),
]; ];
$star = false;
$isstarred = "unstarred";
$lock = false;
$likebuttons = [ $likebuttons = [
'like' => null, 'like' => null,
'dislike' => null, 'dislike' => null,
@ -577,7 +569,7 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
unset($likebuttons['dislike']); unset($likebuttons['dislike']);
} }
$body = Item::prepareBody($item, true, $preview); $body_html = Item::prepareBody($item, true, $preview);
list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item); list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item);
@ -596,13 +588,13 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']), 'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link']),
'linktitle' => DI::l10n()->t('View %s\'s profile @ %s', $profile_name, $item['author-link']), 'linktitle' => DI::l10n()->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
'profile_url' => $profile_link, 'profile_url' => $profile_link,
'item_photo_menu' => item_photo_menu($item), 'item_photo_menu_html' => item_photo_menu($item),
'name' => $profile_name, 'name' => $profile_name,
'sparkle' => $sparkle, 'sparkle' => $sparkle,
'lock' => $lock, 'lock' => false,
'thumb' => DI::baseUrl()->remove($item['author-avatar']), 'thumb' => DI::baseUrl()->remove($item['author-avatar']),
'title' => $title, 'title' => $title,
'body' => $body, 'body_html' => $body_html,
'tags' => $tags['tags'], 'tags' => $tags['tags'],
'hashtags' => $tags['hashtags'], 'hashtags' => $tags['hashtags'],
'mentions' => $tags['mentions'], 'mentions' => $tags['mentions'],
@ -613,23 +605,23 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
'has_folders' => ((count($folders)) ? 'true' : ''), 'has_folders' => ((count($folders)) ? 'true' : ''),
'categories' => $categories, 'categories' => $categories,
'folders' => $folders, 'folders' => $folders,
'text' => strip_tags($body), 'text' => strip_tags($body_html),
'localtime' => DateTimeFormat::local($item['created'], 'r'), 'localtime' => DateTimeFormat::local($item['created'], 'r'),
'ago' => (($item['app']) ? DI::l10n()->t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])), 'ago' => (($item['app']) ? DI::l10n()->t('%s from %s', Temporal::getRelativeDate($item['created']),$item['app']) : Temporal::getRelativeDate($item['created'])),
'location' => $location, 'location_html' => $location_html,
'indent' => '', 'indent' => '',
'owner_name' => $owner_name, 'owner_name' => '',
'owner_url' => $owner_url, 'owner_url' => '',
'owner_photo' => DI::baseUrl()->remove($item['owner-avatar']), 'owner_photo' => DI::baseUrl()->remove($item['owner-avatar']),
'plink' => Item::getPlink($item), 'plink' => Item::getPlink($item),
'edpost' => false, 'edpost' => false,
'isstarred' => $isstarred, 'isstarred' => 'unstarred',
'star' => $star, 'star' => false,
'drop' => $drop, 'drop' => $drop,
'vote' => $likebuttons, 'vote' => $likebuttons,
'like' => '', 'like_html' => '',
'dislike' => '', 'dislike_html' => '',
'comment' => '', 'comment_html' => '',
'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> DI::l10n()->t('View in context')]), 'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> DI::l10n()->t('View in context')]),
'previewing' => $previewing, 'previewing' => $previewing,
'wait' => DI::l10n()->t('Please wait'), 'wait' => DI::l10n()->t('Please wait'),
@ -727,7 +719,12 @@ function conversation_fetch_comments($thread_items, $pinned) {
&& ($row['thr-parent'] == $row['parent-uri']) && ($row['received'] > $received) && ($row['thr-parent'] == $row['parent-uri']) && ($row['received'] > $received)
&& Contact::isSharing($row['author-id'], $row['uid'])) { && Contact::isSharing($row['author-id'], $row['uid'])) {
$direction = ['direction' => 3, 'title' => DI::l10n()->t('%s reshared this.', $row['author-name'])]; $direction = ['direction' => 3, 'title' => DI::l10n()->t('%s reshared this.', $row['author-name'])];
$actor = ['link' => $row['author-link'], 'avatar' => $row['author-avatar'], 'name' => $row['author-name']];
$author = ['uid' => 0, 'id' => $row['author-id'],
'network' => $row['author-network'], 'url' => $row['author-link']];
$url = '<a href="'. htmlentities(Contact::magicLinkByContact($author)) .'">' . htmlentities($row['author-name']) . '</a>';
$actor = ['url' => $url, 'link' => $row['author-link'], 'avatar' => $row['author-avatar'], 'name' => $row['author-name']];
$received = $row['received']; $received = $row['received'];
} }
@ -736,12 +733,45 @@ function conversation_fetch_comments($thread_items, $pinned) {
$direction = ['direction' => 5, 'title' => DI::l10n()->t('%s commented on this.', $row['author-name'])]; $direction = ['direction' => 5, 'title' => DI::l10n()->t('%s commented on this.', $row['author-name'])];
} }
if (($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && ($row['author-id'] == $row['owner-id']) switch ($row['post-type']) {
&& !Contact::isSharing($row['author-id'], $row['uid'])) { case Item::PT_TO:
if ($row['post-type'] == Item::PT_TAG) { $row['direction'] = ['direction' => 7, 'title' => DI::l10n()->t('You had been addressed (%s).', 'to')];
break;
case Item::PT_CC:
$row['direction'] = ['direction' => 7, 'title' => DI::l10n()->t('You had been addressed (%s).', 'cc')];
break;
case Item::PT_BTO:
$row['direction'] = ['direction' => 7, 'title' => DI::l10n()->t('You had been addressed (%s).', 'bto')];
break;
case Item::PT_BCC:
$row['direction'] = ['direction' => 7, 'title' => DI::l10n()->t('You had been addressed (%s).', 'bcc')];
break;
case Item::PT_FOLLOWER:
$row['direction'] = ['direction' => 6, 'title' => DI::l10n()->t('You are following %s.', $row['author-name'])];
break;
case Item::PT_TAG:
$row['direction'] = ['direction' => 4, 'title' => DI::l10n()->t('Tagged')]; $row['direction'] = ['direction' => 4, 'title' => DI::l10n()->t('Tagged')];
} break;
case Item::PT_ANNOUNCEMENT:
$row['direction'] = ['direction' => 3, 'title' => DI::l10n()->t('Reshared')];
break;
case Item::PT_COMMENT:
$row['direction'] = ['direction' => 5, 'title' => DI::l10n()->t('%s is participating in this thread.', $row['author-name'])];
break;
case Item::PT_STORED:
$row['direction'] = ['direction' => 8, 'title' => DI::l10n()->t('Stored')];
break;
case Item::PT_GLOBAL:
$row['direction'] = ['direction' => 9, 'title' => DI::l10n()->t('Global')];
break;
default:
if ($row['uid'] == 0) {
$row['direction'] = ['direction' => 9, 'title' => DI::l10n()->t('Global')];
}
}
if (($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && ($row['author-id'] == $row['owner-id']) &&
!Contact::isSharing($row['author-id'], $row['uid'])) {
$parentlines[] = $lineno; $parentlines[] = $lineno;
} }
@ -758,10 +788,13 @@ function conversation_fetch_comments($thread_items, $pinned) {
if (!empty($direction)) { if (!empty($direction)) {
foreach ($parentlines as $line) { foreach ($parentlines as $line) {
$comments[$line]['direction'] = $direction; $comments[$line]['direction'] = $direction;
if (!empty($actor) && DI::pConfig()->get(local_user(), 'system', 'display_resharer') ) { if (!empty($actor)) {
$comments[$line]['owner-link'] = $actor['link']; $comments[$line]['reshared'] = DI::l10n()->t('%s reshared this.', $actor['url']);
$comments[$line]['owner-avatar'] = $actor['avatar']; if (DI::pConfig()->get(local_user(), 'system', 'display_resharer') ) {
$comments[$line]['owner-name'] = $actor['name']; $comments[$line]['owner-link'] = $actor['link'];
$comments[$line]['owner-avatar'] = $actor['avatar'];
$comments[$line]['owner-name'] = $actor['name'];
}
} }
} }
} }
@ -1137,34 +1170,12 @@ function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
$jotplugins = ''; $jotplugins = '';
Hook::callAll('jot_tool', $jotplugins); Hook::callAll('jot_tool', $jotplugins);
// Private/public post links for the non-JS ACL form
$private_post = 1;
if (!empty($_REQUEST['public'])) {
$private_post = 0;
}
$query_str = DI::args()->getQueryString();
if (strpos($query_str, 'public=1') !== false) {
$query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
}
/*
* I think $a->query_string may never have ? in it, but I could be wrong
* It looks like it's from the index.php?q=[etc] rewrite that the web
* server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
*/
if (strpos($query_str, '?') === false) {
$public_post_link = '?public=1';
} else {
$public_post_link = '&public=1';
}
// $tpl = Renderer::replaceMacros($tpl,array('$jotplugins' => $jotplugins)); // $tpl = Renderer::replaceMacros($tpl,array('$jotplugins' => $jotplugins));
$tpl = Renderer::getMarkupTemplate("jot.tpl"); $tpl = Renderer::getMarkupTemplate("jot.tpl");
$o .= Renderer::replaceMacros($tpl,[ $o .= Renderer::replaceMacros($tpl,[
'$new_post' => DI::l10n()->t('New Post'), '$new_post' => DI::l10n()->t('New Post'),
'$return_path' => $query_str, '$return_path' => DI::args()->getQueryString(),
'$action' => 'item', '$action' => 'item',
'$share' => ($x['button'] ?? '') ?: DI::l10n()->t('Share'), '$share' => ($x['button'] ?? '') ?: DI::l10n()->t('Share'),
'$loading' => DI::l10n()->t('Loading...'), '$loading' => DI::l10n()->t('Loading...'),
@ -1190,7 +1201,7 @@ function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
'$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? DI::l10n()->t("Categories \x28comma-separated list\x29") : '', '$placeholdercategory' => Feature::isEnabled(local_user(), 'categories') ? DI::l10n()->t("Categories \x28comma-separated list\x29") : '',
'$wait' => DI::l10n()->t('Please wait'), '$wait' => DI::l10n()->t('Please wait'),
'$permset' => DI::l10n()->t('Permission settings'), '$permset' => DI::l10n()->t('Permission settings'),
'$shortpermset' => DI::l10n()->t('permissions'), '$shortpermset' => DI::l10n()->t('Permissions'),
'$wall' => $notes_cid ? 0 : 1, '$wall' => $notes_cid ? 0 : 1,
'$posttype' => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE, '$posttype' => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
'$content' => $x['content'] ?? '', '$content' => $x['content'] ?? '',
@ -1212,11 +1223,6 @@ function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
// ACL permissions box // ACL permissions box
'$acl' => $x['acl'], '$acl' => $x['acl'],
'$group_perms' => DI::l10n()->t('Post to Groups'),
'$contact_perms' => DI::l10n()->t('Post to Contacts'),
'$private' => DI::l10n()->t('Private post'),
'$is_private' => $private_post,
'$public_link' => $public_post_link,
//jot nav tab (used in some themes) //jot nav tab (used in some themes)
'$message' => DI::l10n()->t('Message'), '$message' => DI::l10n()->t('Message'),
@ -1490,13 +1496,3 @@ function sort_thr_commented(array $a, array $b)
{ {
return strcmp($b['commented'], $a['commented']); return strcmp($b['commented'], $a['commented']);
} }
function render_location_dummy(array $item) {
if (!empty($item['location']) && !empty($item['location'])) {
return $item['location'];
}
if (!empty($item['coord']) && !empty($item['coord'])) {
return $item['coord'];
}
}

View file

@ -87,12 +87,15 @@ function notification($params)
} }
$nickname = $user["nickname"]; $nickname = $user["nickname"];
// Creates a new email builder for the notification email
$emailBuilder = DI::emailer()->newNotifyMail();
// with $params['show_in_notification_page'] == false, the notification isn't inserted into // with $params['show_in_notification_page'] == false, the notification isn't inserted into
// the database, and an email is sent if applicable. // the database, and an email is sent if applicable.
// default, if not specified: true // default, if not specified: true
$show_in_notification_page = isset($params['show_in_notification_page']) ? $params['show_in_notification_page'] : true; $show_in_notification_page = isset($params['show_in_notification_page']) ? $params['show_in_notification_page'] : true;
$additional_mail_header = "X-Friendica-Account: <".$nickname."@".$hostname.">\n"; $emailBuilder->setHeader('X-Friendica-Account', '<' . $nickname . '@' . $hostname . '>');
if (array_key_exists('item', $params)) { if (array_key_exists('item', $params)) {
$title = $params['item']['title']; $title = $params['item']['title'];
@ -509,7 +512,8 @@ function notification($params)
Logger::log('sending notification email'); Logger::log('sending notification email');
if (isset($params['parent']) && (intval($params['parent']) != 0)) { if (isset($params['parent']) && (intval($params['parent']) != 0)) {
$id_for_parent = $params['parent'] . "@" . $hostname; $parent = Item::selectFirst(['guid'], ['id' => $params['parent']]);
$message_id = "<" . $parent['guid'] . "@" . gethostname() . ">";
// Is this the first email notification for this parent item and user? // Is this the first email notification for this parent item and user?
if (!DBA::exists('notify-threads', ['master-parent-item' => $params['parent'], 'receiver-uid' => $params['uid']])) { if (!DBA::exists('notify-threads', ['master-parent-item' => $params['parent'], 'receiver-uid' => $params['uid']])) {
@ -520,13 +524,14 @@ function notification($params)
'receiver-uid' => $params['uid'], 'parent-item' => 0]; 'receiver-uid' => $params['uid'], 'parent-item' => 0];
DBA::insert('notify-threads', $fields); DBA::insert('notify-threads', $fields);
$additional_mail_header .= "Message-ID: <${id_for_parent}>\n"; $emailBuilder->setHeader('Message-ID', $message_id);
$log_msg = "include/enotify: No previous notification found for this parent:\n" . $log_msg = "include/enotify: No previous notification found for this parent:\n" .
" parent: ${params['parent']}\n" . " uid : ${params['uid']}\n"; " parent: ${params['parent']}\n" . " uid : ${params['uid']}\n";
Logger::log($log_msg, Logger::DEBUG); Logger::log($log_msg, Logger::DEBUG);
} else { } else {
// If not, just "follow" the thread. // If not, just "follow" the thread.
$additional_mail_header .= "References: <${id_for_parent}>\nIn-Reply-To: <${id_for_parent}>\n"; $emailBuilder->setHeader('References', $message_id);
$emailBuilder->setHeader('In-Reply-To', $message_id);
Logger::log("There's already a notification for this parent.", Logger::DEBUG); Logger::log("There's already a notification for this parent.", Logger::DEBUG);
} }
} }
@ -545,7 +550,6 @@ function notification($params)
'title' => $title, 'title' => $title,
'body' => $body, 'body' => $body,
'subject' => $subject, 'subject' => $subject,
'headers' => $additional_mail_header,
]; ];
Hook::callAll('enotify_mail', $datarray); Hook::callAll('enotify_mail', $datarray);
@ -564,13 +568,13 @@ function notification($params)
// If a photo is present, add it to the email // If a photo is present, add it to the email
if (!empty($datarray['source_photo'])) { if (!empty($datarray['source_photo'])) {
$builder->withPhoto( $emailBuilder->withPhoto(
$datarray['source_photo'], $datarray['source_photo'],
$datarray['source_link'] ?? $sitelink, $datarray['source_link'] ?? $sitelink,
$datarray['source_name'] ?? $sitename); $datarray['source_name'] ?? $sitename);
} }
$email = $builder->build(); $email = $emailBuilder->build();
// use the Emailer class to send the message // use the Emailer class to send the message
return DI::emailer()->send($email); return DI::emailer()->send($email);

View file

@ -131,7 +131,7 @@ function editpost_content(App $a)
//jot nav tab (used in some themes) //jot nav tab (used in some themes)
'$message' => DI::l10n()->t('Message'), '$message' => DI::l10n()->t('Message'),
'$browser' => DI::l10n()->t('Browser'), '$browser' => DI::l10n()->t('Browser'),
'$shortpermset' => DI::l10n()->t('permissions'), '$shortpermset' => DI::l10n()->t('Permissions'),
'$compose_link_title' => DI::l10n()->t('Open Compose page'), '$compose_link_title' => DI::l10n()->t('Open Compose page'),
]); ]);

View file

@ -474,16 +474,16 @@ function events_content(App $a)
$t_orig = $orig_event['summary'] ?? ''; $t_orig = $orig_event['summary'] ?? '';
$d_orig = $orig_event['desc'] ?? ''; $d_orig = $orig_event['desc'] ?? '';
$l_orig = $orig_event['location'] ?? ''; $l_orig = $orig_event['location'] ?? '';
$eid = !empty($orig_event) ? $orig_event['id'] : 0; $eid = $orig_event['id'] ?? 0;
$cid = !empty($orig_event) ? $orig_event['cid'] : 0; $cid = $orig_event['cid'] ?? 0;
$uri = !empty($orig_event) ? $orig_event['uri'] : ''; $uri = $orig_event['uri'] ?? '';
if ($cid || $mode === 'edit') { if ($cid || $mode === 'edit') {
$share_disabled = 'disabled="disabled"'; $share_disabled = 'disabled="disabled"';
} }
$sdt = !empty($orig_event) ? $orig_event['start'] : 'now'; $sdt = $orig_event['start'] ?? 'now';
$fdt = !empty($orig_event) ? $orig_event['finish'] : 'now'; $fdt = $orig_event['finish'] ?? 'now';
$tz = date_default_timezone_get(); $tz = date_default_timezone_get();
if (!empty($orig_event)) { if (!empty($orig_event)) {

View file

@ -260,7 +260,7 @@ function item_post(App $a) {
$objecttype = $orig_post['object-type']; $objecttype = $orig_post['object-type'];
$app = $orig_post['app']; $app = $orig_post['app'];
$categories = $orig_post['file'] ?? ''; $categories = $orig_post['file'] ?? '';
$title = Strings::escapeTags(trim($_REQUEST['title'])); $title = trim($_REQUEST['title'] ?? '');
$body = trim($body); $body = trim($body);
$private = $orig_post['private']; $private = $orig_post['private'];
$pubmail_enabled = $orig_post['pubmail']; $pubmail_enabled = $orig_post['pubmail'];
@ -281,13 +281,13 @@ function item_post(App $a) {
$str_group_deny = isset($_REQUEST['group_deny']) ? $aclFormatter->toString($_REQUEST['group_deny']) : $user['deny_gid'] ?? ''; $str_group_deny = isset($_REQUEST['group_deny']) ? $aclFormatter->toString($_REQUEST['group_deny']) : $user['deny_gid'] ?? '';
} }
$title = Strings::escapeTags(trim($_REQUEST['title'] ?? '')); $title = trim($_REQUEST['title'] ?? '');
$location = Strings::escapeTags(trim($_REQUEST['location'] ?? '')); $location = trim($_REQUEST['location'] ?? '');
$coord = Strings::escapeTags(trim($_REQUEST['coord'] ?? '')); $coord = trim($_REQUEST['coord'] ?? '');
$verb = Strings::escapeTags(trim($_REQUEST['verb'] ?? '')); $verb = trim($_REQUEST['verb'] ?? '');
$emailcc = Strings::escapeTags(trim($_REQUEST['emailcc'] ?? '')); $emailcc = trim($_REQUEST['emailcc'] ?? '');
$body = trim($body); $body = trim($body);
$network = Strings::escapeTags(trim(($_REQUEST['network'] ?? '') ?: Protocol::DFRN)); $network = trim(($_REQUEST['network'] ?? '') ?: Protocol::DFRN);
$guid = System::createUUID(); $guid = System::createUUID();
$postopts = $_REQUEST['postopts'] ?? ''; $postopts = $_REQUEST['postopts'] ?? '';
@ -904,40 +904,8 @@ function drop_item(int $id, string $return = '')
} }
if ((local_user() == $item['uid']) || $contact_id) { if ((local_user() == $item['uid']) || $contact_id) {
// Check if we should do HTML-based delete confirmation
if (!empty($_REQUEST['confirm'])) {
// <form> can't take arguments in its "action" parameter
// so add any arguments as hidden inputs
$query = explode_querystring(DI::args()->getQueryString());
$inputs = [];
foreach ($query['args'] as $arg) {
if (strpos($arg, 'confirm=') === false) {
$arg_parts = explode('=', $arg);
$inputs[] = ['name' => $arg_parts[0], 'value' => $arg_parts[1]];
}
}
return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [
'$method' => 'get',
'$message' => DI::l10n()->t('Do you really want to delete this item?'),
'$extra_inputs' => $inputs,
'$confirm' => DI::l10n()->t('Yes'),
'$confirm_url' => $query['base'],
'$confirm_name' => 'confirmed',
'$cancel' => DI::l10n()->t('Cancel'),
]);
}
// Now check how the user responded to the confirmation query
if (!empty($_REQUEST['canceled'])) {
DI::baseUrl()->redirect('display/' . $item['guid']);
}
$is_comment = $item['gravity'] == GRAVITY_COMMENT;
$parentitem = null;
if (!empty($item['parent'])) { if (!empty($item['parent'])) {
$fields = ['guid']; $parentitem = Item::selectFirstForUser(local_user(), ['guid'], ['id' => $item['parent']]);
$parentitem = Item::selectFirstForUser(local_user(), $fields, ['id' => $item['parent']]);
} }
// delete the item // delete the item
@ -949,7 +917,7 @@ function drop_item(int $id, string $return = '')
$return_url = str_replace("update_", "", $return_url); $return_url = str_replace("update_", "", $return_url);
// Check if delete a comment // Check if delete a comment
if ($is_comment) { if ($item['gravity'] == GRAVITY_COMMENT) {
// Return to parent guid // Return to parent guid
if (!empty($parentitem)) { if (!empty($parentitem)) {
DI::baseUrl()->redirect('display/' . $parentitem['guid']); DI::baseUrl()->redirect('display/' . $parentitem['guid']);

View file

@ -141,36 +141,6 @@ function message_content(App $a)
return; return;
} }
// Check if we should do HTML-based delete confirmation
if (!empty($_REQUEST['confirm'])) {
// <form> can't take arguments in its "action" parameter
// so add any arguments as hidden inputs
$query = explode_querystring(DI::args()->getQueryString());
$inputs = [];
foreach ($query['args'] as $arg) {
if (strpos($arg, 'confirm=') === false) {
$arg_parts = explode('=', $arg);
$inputs[] = ['name' => $arg_parts[0], 'value' => $arg_parts[1]];
}
}
//DI::page()['aside'] = '';
return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [
'$method' => 'get',
'$message' => DI::l10n()->t('Do you really want to delete this message?'),
'$extra_inputs' => $inputs,
'$confirm' => DI::l10n()->t('Yes'),
'$confirm_url' => $query['base'],
'$confirm_name' => 'confirmed',
'$cancel' => DI::l10n()->t('Cancel'),
]);
}
// Now check how the user responded to the confirmation query
if (!empty($_REQUEST['canceled'])) {
DI::baseUrl()->redirect('message');
}
$cmd = $a->argv[1]; $cmd = $a->argv[1];
if ($cmd === 'drop') { if ($cmd === 'drop') {
$message = DBA::selectFirst('mail', ['convid'], ['id' => $a->argv[2], 'uid' => local_user()]); $message = DBA::selectFirst('mail', ['convid'], ['id' => $a->argv[2], 'uid' => local_user()]);

View file

@ -635,9 +635,7 @@ function network_display_post($a, $pager, $mark_all, $update, $ordering, $items)
$parents_str = implode(', ', $parents_arr); $parents_str = implode(', ', $parents_arr);
} }
$query_string = DI::args()->getQueryString(); $pager->setQueryString(DI::args()->getQueryString());
$pager->setQueryString($query_string);
// We aren't going to try and figure out at the item, group, and page // We aren't going to try and figure out at the item, group, and page
// level which items you've seen and which you haven't. If you're looking // level which items you've seen and which you haven't. If you're looking

View file

@ -55,7 +55,7 @@ function notes_content(App $a, $update = false)
'default_location' => $a->user['default-location'], 'default_location' => $a->user['default-location'],
'nickname' => $a->user['nickname'], 'nickname' => $a->user['nickname'],
'lockstate' => 'lock', 'lockstate' => 'lock',
'acl' => '', 'acl' => \Friendica\Core\ACL::getSelfOnlyHTML(local_user(), DI::l10n()->t('Personal notes are visible only by yourself.')),
'bang' => '', 'bang' => '',
'visitor' => 'block', 'visitor' => 'block',
'profile_uid' => local_user(), 'profile_uid' => local_user(),

View file

@ -25,6 +25,7 @@ use Friendica\Content\Nav;
use Friendica\Content\Pager; use Friendica\Content\Pager;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Core\ACL; use Friendica\Core\ACL;
use Friendica\Core\Addon;
use Friendica\Core\Hook; use Friendica\Core\Hook;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
@ -987,8 +988,6 @@ function photos_content(App $a)
'$uploadurl' => $ret['post_url'], '$uploadurl' => $ret['post_url'],
// ACL permissions box // ACL permissions box
'$group_perms' => DI::l10n()->t('Show to Groups'),
'$contact_perms' => DI::l10n()->t('Show to Contacts'),
'$return_path' => DI::args()->getQueryString(), '$return_path' => DI::args()->getQueryString(),
]); ]);
@ -1040,7 +1039,6 @@ function photos_content(App $a)
return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [
'$method' => 'post', '$method' => 'post',
'$message' => DI::l10n()->t('Do you really want to delete this photo album and all its photos?'), '$message' => DI::l10n()->t('Do you really want to delete this photo album and all its photos?'),
'$extra_inputs' => [],
'$confirm' => DI::l10n()->t('Delete Album'), '$confirm' => DI::l10n()->t('Delete Album'),
'$confirm_url' => $drop_url, '$confirm_url' => $drop_url,
'$confirm_name' => 'dropalbum', '$confirm_name' => 'dropalbum',
@ -1147,7 +1145,6 @@ function photos_content(App $a)
return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [
'$method' => 'post', '$method' => 'post',
'$message' => DI::l10n()->t('Do you really want to delete this photo?'), '$message' => DI::l10n()->t('Do you really want to delete this photo?'),
'$extra_inputs' => [],
'$confirm' => DI::l10n()->t('Delete Photo'), '$confirm' => DI::l10n()->t('Delete Photo'),
'$confirm_url' => $drop_url, '$confirm_url' => $drop_url,
'$confirm_name' => 'delete', '$confirm_name' => 'delete',
@ -1352,8 +1349,6 @@ function photos_content(App $a)
'$delete' => DI::l10n()->t('Delete Photo'), '$delete' => DI::l10n()->t('Delete Photo'),
// ACL permissions box // ACL permissions box
'$group_perms' => DI::l10n()->t('Show to Groups'),
'$contact_perms' => DI::l10n()->t('Show to Contacts'),
'$return_path' => DI::args()->getQueryString(), '$return_path' => DI::args()->getQueryString(),
]); ]);
} }
@ -1382,6 +1377,16 @@ function photos_content(App $a)
if (!DBA::isResult($items)) { if (!DBA::isResult($items)) {
if (($can_post || Security::canWriteToUserWall($owner_uid))) { if (($can_post || Security::canWriteToUserWall($owner_uid))) {
/*
* Hmmm, code depending on the presence of a particular addon?
* This should be better if done by a hook
*/
$qcomment = null;
if (Addon::isEnabled('qcomment')) {
$words = DI::pConfig()->get(local_user(), 'qcomment', 'words');
$qcomment = $words ? explode("\n", $words) : [];
}
$comments .= Renderer::replaceMacros($cmnt_tpl, [ $comments .= Renderer::replaceMacros($cmnt_tpl, [
'$return_path' => '', '$return_path' => '',
'$jsreload' => $return_path, '$jsreload' => $return_path,
@ -1396,7 +1401,7 @@ function photos_content(App $a)
'$preview' => DI::l10n()->t('Preview'), '$preview' => DI::l10n()->t('Preview'),
'$loading' => DI::l10n()->t('Loading...'), '$loading' => DI::l10n()->t('Loading...'),
'$sourceapp' => DI::l10n()->t($a->sourcename), '$sourceapp' => DI::l10n()->t($a->sourcename),
'$ww' => '', '$qcomment' => $qcomment,
'$rand_num' => Crypto::randomDigits(12) '$rand_num' => Crypto::randomDigits(12)
]); ]);
} }
@ -1429,6 +1434,16 @@ function photos_content(App $a)
} }
if (($can_post || Security::canWriteToUserWall($owner_uid))) { if (($can_post || Security::canWriteToUserWall($owner_uid))) {
/*
* Hmmm, code depending on the presence of a particular addon?
* This should be better if done by a hook
*/
$qcomment = null;
if (Addon::isEnabled('qcomment')) {
$words = DI::pConfig()->get(local_user(), 'qcomment', 'words');
$qcomment = $words ? explode("\n", $words) : [];
}
$comments .= Renderer::replaceMacros($cmnt_tpl,[ $comments .= Renderer::replaceMacros($cmnt_tpl,[
'$return_path' => '', '$return_path' => '',
'$jsreload' => $return_path, '$jsreload' => $return_path,
@ -1442,7 +1457,7 @@ function photos_content(App $a)
'$submit' => DI::l10n()->t('Submit'), '$submit' => DI::l10n()->t('Submit'),
'$preview' => DI::l10n()->t('Preview'), '$preview' => DI::l10n()->t('Preview'),
'$sourceapp' => DI::l10n()->t($a->sourcename), '$sourceapp' => DI::l10n()->t($a->sourcename),
'$ww' => '', '$qcomment' => $qcomment,
'$rand_num' => Crypto::randomDigits(12) '$rand_num' => Crypto::randomDigits(12)
]); ]);
} }
@ -1492,6 +1507,16 @@ function photos_content(App $a)
]); ]);
if (($can_post || Security::canWriteToUserWall($owner_uid))) { if (($can_post || Security::canWriteToUserWall($owner_uid))) {
/*
* Hmmm, code depending on the presence of a particular addon?
* This should be better if done by a hook
*/
$qcomment = null;
if (Addon::isEnabled('qcomment')) {
$words = DI::pConfig()->get(local_user(), 'qcomment', 'words');
$qcomment = $words ? explode("\n", $words) : [];
}
$comments .= Renderer::replaceMacros($cmnt_tpl, [ $comments .= Renderer::replaceMacros($cmnt_tpl, [
'$return_path' => '', '$return_path' => '',
'$jsreload' => $return_path, '$jsreload' => $return_path,
@ -1505,7 +1530,7 @@ function photos_content(App $a)
'$submit' => DI::l10n()->t('Submit'), '$submit' => DI::l10n()->t('Submit'),
'$preview' => DI::l10n()->t('Preview'), '$preview' => DI::l10n()->t('Preview'),
'$sourceapp' => DI::l10n()->t($a->sourcename), '$sourceapp' => DI::l10n()->t($a->sourcename),
'$ww' => '', '$qcomment' => $qcomment,
'$rand_num' => Crypto::randomDigits(12) '$rand_num' => Crypto::randomDigits(12)
]); ]);
} }

View file

@ -829,26 +829,6 @@ function settings_content(App $a)
$stpl = Renderer::getMarkupTemplate('settings/settings.tpl'); $stpl = Renderer::getMarkupTemplate('settings/settings.tpl');
// Private/public post links for the non-JS ACL form
$private_post = 1;
if (!empty($_REQUEST['public']) && !$_REQUEST['public']) {
$private_post = 0;
}
$query_str = DI::args()->getQueryString();
if (strpos($query_str, 'public=1') !== false) {
$query_str = str_replace(['?public=1', '&public=1'], ['', ''], $query_str);
}
// I think $a->query_string may never have ? in it, but I could be wrong
// It looks like it's from the index.php?q=[etc] rewrite that the web
// server does, which converts any ? to &, e.g. suggest&ignore=61 for suggest?ignore=61
if (strpos($query_str, '?') === false) {
$public_post_link = '?public=1';
} else {
$public_post_link = '&public=1';
}
/* Installed langs */ /* Installed langs */
$lang_choices = DI::l10n()->getAvailableLanguages(); $lang_choices = DI::l10n()->getAvailableLanguages();

View file

@ -105,6 +105,9 @@ $HTTP["scheme"] == "https" {
"^\/([^\?]*)\?(.*)$" => "/index.php?pagename=$1&$2", "^\/([^\?]*)\?(.*)$" => "/index.php?pagename=$1&$2",
"^\/(.*)$" => "/index.php?pagename=$1" "^\/(.*)$" => "/index.php?pagename=$1"
) )
$HOST["url"] =~ "^/bin/" {
url.access.deny ( "" )
}
} }
else $HTTP["host"] !~ "(friendica.example.com|wordpress.example.com)" { else $HTTP["host"] !~ "(friendica.example.com|wordpress.example.com)" {
server.document-root = "/var/www/wordpress" server.document-root = "/var/www/wordpress"

View file

@ -141,4 +141,9 @@ server {
location ~ /\. { location ~ /\. {
deny all; deny all;
} }
# deny access to the CLI scripts
location ^~ /bin {
deny all;
}
} }

View file

@ -47,7 +47,7 @@ class Arguments
*/ */
private $argc; private $argc;
public function __construct(string $queryString = '', string $command = '', array $argv = [Module::DEFAULT], int $argc = 1) public function __construct(string $queryString = '', string $command = '', array $argv = [], int $argc = 0)
{ {
$this->queryString = $queryString; $this->queryString = $queryString;
$this->command = $command; $this->command = $command;
@ -56,7 +56,7 @@ class Arguments
} }
/** /**
* @return string The whole query string of this call * @return string The whole query string of this call with url-encoded query parameters
*/ */
public function getQueryString() public function getQueryString()
{ {
@ -121,50 +121,27 @@ class Arguments
*/ */
public function determine(array $server, array $get) public function determine(array $server, array $get)
{ {
$queryString = ''; // removing leading / - maybe a nginx problem
$server['QUERY_STRING'] = ltrim($server['QUERY_STRING'] ?? '', '/');
if (!empty($server['QUERY_STRING']) && strpos($server['QUERY_STRING'], 'pagename=') === 0) { $queryParameters = [];
$queryString = urldecode(substr($server['QUERY_STRING'], 9)); parse_str($server['QUERY_STRING'], $queryParameters);
} elseif (!empty($server['QUERY_STRING']) && strpos($server['QUERY_STRING'], 'q=') === 0) {
$queryString = urldecode(substr($server['QUERY_STRING'], 2));
}
// eventually strip ZRL
$queryString = $this->stripZRLs($queryString);
// eventually strip OWT
$queryString = $this->stripQueryParam($queryString, 'owt');
// removing trailing / - maybe a nginx problem
$queryString = ltrim($queryString, '/');
if (!empty($get['pagename'])) { if (!empty($get['pagename'])) {
$command = trim($get['pagename'], '/\\'); $command = trim($get['pagename'], '/\\');
} elseif (!empty($queryParameters['pagename'])) {
$command = trim($queryParameters['pagename'], '/\\');
} elseif (!empty($get['q'])) { } elseif (!empty($get['q'])) {
// Legacy page name parameter, now conflicts with the search query parameter
$command = trim($get['q'], '/\\'); $command = trim($get['q'], '/\\');
} else { } else {
$command = Module::DEFAULT; $command = '';
} }
// Remove generated and one-time use parameters
// fix query_string unset($queryParameters['pagename']);
if (!empty($command)) { unset($queryParameters['zrl']);
$queryString = str_replace( unset($queryParameters['owt']);
$command . '&',
$command . '?',
$queryString
);
}
// unix style "homedir"
if (substr($command, 0, 1) === '~') {
$command = 'profile/' . substr($command, 1);
}
// Diaspora style profile url
if (substr($command, 0, 2) === 'u/') {
$command = 'profile/' . substr($command, 2);
}
/* /*
* Break the URL path into C style argc/argv style arguments for our * Break the URL path into C style argc/argv style arguments for our
@ -173,41 +150,17 @@ class Arguments
* [0] => 'module' * [0] => 'module'
* [1] => 'arg1' * [1] => 'arg1'
* [2] => 'arg2' * [2] => 'arg2'
*
*
* There will always be one argument. If provided a naked domain
* URL, $this->argv[0] is set to "home".
*/ */
if ($command) {
$argv = explode('/', $command);
} else {
$argv = [];
}
$argv = explode('/', $command);
$argc = count($argv); $argc = count($argv);
$queryString = $command . ($queryParameters ? '?' . http_build_query($queryParameters) : '');
return new Arguments($queryString, $command, $argv, $argc); return new Arguments($queryString, $command, $argv, $argc);
} }
/**
* Strip zrl parameter from a string.
*
* @param string $queryString The input string.
*
* @return string The zrl.
*/
public function stripZRLs(string $queryString)
{
return preg_replace('/[?&]zrl=(.*?)(&|$)/ism', '$2', $queryString);
}
/**
* Strip query parameter from a string.
*
* @param string $queryString The input string.
* @param string $param
*
* @return string The query parameter.
*/
public function stripQueryParam(string $queryString, string $param)
{
return preg_replace('/[?&]' . $param . '=(.*?)(&|$)/ism', '$2', $queryString);
}
} }

View file

@ -55,6 +55,7 @@ Commands
update Update database schema update Update database schema
dumpsql Dump database schema dumpsql Dump database schema
toinnodb Convert all tables from MyISAM or InnoDB in the Antelope file format to InnoDB in the Barracuda file format toinnodb Convert all tables from MyISAM or InnoDB in the Antelope file format to InnoDB in the Barracuda file format
version Set the database to a given number
Options Options
-h|--help|-? Show help information -h|--help|-? Show help information
@ -86,8 +87,10 @@ HELP;
return 0; return 0;
} }
if (count($this->args) > 1) { if ((count($this->args) > 1) && ($this->getArgument(0) != 'version')) {
throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments'); throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments');
} elseif ((count($this->args) != 2) && ($this->getArgument(0) == 'version')) {
throw new \Asika\SimpleConsole\CommandArgsException('This command needs two arguments');
} }
if (!$this->dba->isConnected()) { if (!$this->dba->isConnected()) {
@ -115,6 +118,12 @@ HELP;
DBStructure::convertToInnoDB(); DBStructure::convertToInnoDB();
$output = ob_get_clean(); $output = ob_get_clean();
break; break;
case "version":
ob_start();
DBStructure::setDatabaseVersion($this->getArgument(1));
$output = ob_get_clean();
break;
default: default:
$output = 'Unknown command: ' . $this->getArgument(0); $output = 'Unknown command: ' . $this->getArgument(0);
} }

134
src/Console/Relay.php Normal file
View file

@ -0,0 +1,134 @@
<?php
/**
* @copyright Copyright (C) 2020, Friendica
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Console;
use Asika\SimpleConsole\CommandArgsException;
use Friendica\Model\APContact;
use Friendica\Model\Contact;
use Friendica\Protocol\ActivityPub\Transmitter;
/**
* tool to control the list of ActivityPub relay servers from the CLI
*
* With this script you can access the relay servers of your node from
* the CLI.
*/
class Relay extends \Asika\SimpleConsole\Console
{
protected $helpOptions = ['h', 'help', '?'];
/**
* @var $dba Friendica\Database\Database
*/
private $dba;
protected function getHelp()
{
$help = <<<HELP
console relay - Manage ActivityPub relay configuration
Synopsis
bin/console relay [-h|--help|-?] [-v]
bin/console relay add <actor> [-h|--help|-?] [-v]
bin/console relay remove <actoor> [-h|--help|-?] [-v]
Description
bin/console relay
Lists all active relay servers
bin/console relay add <actor>
Add a relay actor in the format https://relayserver.tld/actor
bin/console relay remove <actor>
Remove a relay actor in the format https://relayserver.tld/actor
Options
-h|--help|-? Show help information
-v Show more debug information.
HELP;
return $help;
}
public function __construct(\Friendica\Database\Database $dba, array $argv = null)
{
parent::__construct($argv);
$this->dba = $dba;
}
protected function doExecute()
{
if ($this->getOption('v')) {
$this->out('Executable: ' . $this->executable);
$this->out('Class: ' . __CLASS__);
$this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true));
}
if (count($this->args) > 2) {
throw new CommandArgsException('Too many arguments');
}
if (count($this->args) == 1) {
throw new CommandArgsException('Too few arguments');
}
if (count($this->args) == 0) {
$contacts = $this->dba->select('apcontact', ['url'],
["`type` = ? AND `url` IN (SELECT `url` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?))",
'Application', 0, Contact::FOLLOWER, Contact::FRIEND]);
while ($contact = $this->dba->fetch($contacts)) {
$this->out($contact['url']);
}
$this->dba->close($contacts);
}
if (count($this->args) == 2) {
$mode = $this->getArgument(0);
$actor = $this->getArgument(1);
$apcontact = APContact::getByURL($actor);
if (empty($apcontact) || ($apcontact['type'] != 'Application')) {
$this->out($actor . ' is no relay actor');
return 1;
}
if ($mode == 'add') {
if (Transmitter::sendRelayFollow($actor)) {
$this->out('Successfully added ' . $actor);
} else {
$this->out($actor . " couldn't be added");
}
} elseif ($mode == 'remove') {
if (Transmitter::sendRelayUndoFollow($actor)) {
$this->out('Successfully removed ' . $actor);
} else {
$this->out($actor . " couldn't be removed");
}
} else {
throw new CommandArgsException($mode . ' is no valid command');
}
}
return 0;
}
}

View file

@ -134,14 +134,6 @@ class PageInfo
$text = "[attachment type='" . $data['type'] . "'"; $text = "[attachment type='" . $data['type'] . "'";
if (empty($data['text'])) {
$data['text'] = $data['title'];
}
if (empty($data['text'])) {
$data['text'] = $data['url'];
}
if (!empty($data['url'])) { if (!empty($data['url'])) {
$text .= " url='" . $data['url'] . "'"; $text .= " url='" . $data['url'] . "'";
} }
@ -150,6 +142,10 @@ class PageInfo
$text .= " title='" . $data['title'] . "'"; $text .= " title='" . $data['title'] . "'";
} }
if (empty($data['text'])) {
$data['text'] = '';
}
// Only embedd a picture link when it seems to be a valid picture ("width" is set) // Only embedd a picture link when it seems to be a valid picture ("width" is set)
if (!empty($data['images']) && !empty($data['images'][0]['width'])) { if (!empty($data['images']) && !empty($data['images'][0]['width'])) {
$preview = str_replace(['[', ']'], ['&#91;', '&#93;'], htmlentities($data['images'][0]['src'], ENT_QUOTES, 'UTF-8', false)); $preview = str_replace(['[', ']'], ['&#91;', '&#93;'], htmlentities($data['images'][0]['src'], ENT_QUOTES, 'UTF-8', false));
@ -160,6 +156,14 @@ class PageInfo
$text .= " image='" . $preview . "'"; $text .= " image='" . $preview . "'";
} else { } else {
$text .= " preview='" . $preview . "'"; $text .= " preview='" . $preview . "'";
if (empty($data['text'])) {
$data['text'] = $data['title'];
}
if (empty($data['text'])) {
$data['text'] = $data['url'];
}
} }
} }

View file

@ -128,7 +128,7 @@ class Pager
/** /**
* Sets the base query string from a full query string. * Sets the base query string from a full query string.
* *
* Strips the 'page' parameter, and remove the 'q=' string for some reason. * Strips the 'page' parameter
* *
* @param string $queryString * @param string $queryString
*/ */

View file

@ -83,6 +83,27 @@ class ACL
return $o; return $o;
} }
/**
* Returns a minimal ACL block for self-only permissions
*
* @param int $localUserId
* @param string $explanation
* @return string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getSelfOnlyHTML(int $localUserId, string $explanation)
{
$selfPublicContactId = Contact::getPublicIdByUserId($localUserId);
$tpl = Renderer::getMarkupTemplate('acl/self_only.tpl');
$o = Renderer::replaceMacros($tpl, [
'$selfPublicContactId' => $selfPublicContactId,
'$explanation' => $explanation,
]);
return $o;
}
/** /**
* Return the default permission of the provided user array * Return the default permission of the provided user array
* *

View file

@ -229,8 +229,6 @@ class Addon
*/ */
public static function getInfo($addon) public static function getInfo($addon)
{ {
$a = DI::app();
$addon = Strings::sanitizeFilePathItem($addon); $addon = Strings::sanitizeFilePathItem($addon);
$info = [ $info = [

View file

@ -64,6 +64,7 @@ Commands:
postupdate Execute pending post update scripts (can last days) postupdate Execute pending post update scripts (can last days)
serverblock Manage blocked servers serverblock Manage blocked servers
storage Manage storage backend storage Manage storage backend
relay Manage ActivityPub relay servers
Options: Options:
-h|--help|-? Show help information -h|--help|-? Show help information
@ -92,6 +93,7 @@ HELP;
'postupdate' => Friendica\Console\PostUpdate::class, 'postupdate' => Friendica\Console\PostUpdate::class,
'serverblock' => Friendica\Console\ServerBlock::class, 'serverblock' => Friendica\Console\ServerBlock::class,
'storage' => Friendica\Console\Storage::class, 'storage' => Friendica\Console\Storage::class,
'relay' => Friendica\Console\Relay::class,
]; ];
/** /**

View file

@ -68,7 +68,7 @@ class Worker
// At first check the maximum load. We shouldn't continue with a high load // At first check the maximum load. We shouldn't continue with a high load
if (DI::process()->isMaxLoadReached()) { if (DI::process()->isMaxLoadReached()) {
Logger::info('Pre check: maximum load reached, quitting.'); Logger::notice('Pre check: maximum load reached, quitting.');
return; return;
} }
@ -134,7 +134,7 @@ class Worker
// Check free memory // Check free memory
if (DI::process()->isMinMemoryReached()) { if (DI::process()->isMinMemoryReached()) {
Logger::info('Memory limit reached, quitting.'); Logger::notice('Memory limit reached, quitting.');
DI::lock()->release(self::LOCK_WORKER); DI::lock()->release(self::LOCK_WORKER);
return; return;
} }
@ -176,19 +176,19 @@ class Worker
// Do we have too few memory? // Do we have too few memory?
if (DI::process()->isMinMemoryReached()) { if (DI::process()->isMinMemoryReached()) {
Logger::info('Memory limit reached, quitting.'); Logger::notice('Memory limit reached, quitting.');
return false; return false;
} }
// Possibly there are too much database connections // Possibly there are too much database connections
if (self::maxConnectionsReached()) { if (self::maxConnectionsReached()) {
Logger::info('Maximum connections reached, quitting.'); Logger::notice('Maximum connections reached, quitting.');
return false; return false;
} }
// Possibly there are too much database processes that block the system // Possibly there are too much database processes that block the system
if (DI::process()->isMaxProcessesReached()) { if (DI::process()->isMaxProcessesReached()) {
Logger::info('Maximum processes reached, quitting.'); Logger::notice('Maximum processes reached, quitting.');
return false; return false;
} }
@ -286,25 +286,25 @@ class Worker
// Quit when in maintenance // Quit when in maintenance
if (DI::config()->get('system', 'maintenance', false, true)) { if (DI::config()->get('system', 'maintenance', false, true)) {
Logger::info("Maintenance mode - quit process", ['pid' => $mypid]); Logger::notice("Maintenance mode - quit process", ['pid' => $mypid]);
return false; return false;
} }
// Constantly check the number of parallel database processes // Constantly check the number of parallel database processes
if (DI::process()->isMaxProcessesReached()) { if (DI::process()->isMaxProcessesReached()) {
Logger::info("Max processes reached for process", ['pid' => $mypid]); Logger::notice("Max processes reached for process", ['pid' => $mypid]);
return false; return false;
} }
// Constantly check the number of available database connections to let the frontend be accessible at any time // Constantly check the number of available database connections to let the frontend be accessible at any time
if (self::maxConnectionsReached()) { if (self::maxConnectionsReached()) {
Logger::info("Max connection reached for process", ['pid' => $mypid]); Logger::notice("Max connection reached for process", ['pid' => $mypid]);
return false; return false;
} }
$argv = json_decode($queue["parameter"], true); $argv = json_decode($queue["parameter"], true);
if (empty($argv)) { if (empty($argv)) {
Logger::error('Parameter is empty', ['queue' => $queue]); Logger::warning('Parameter is empty', ['queue' => $queue]);
return false; return false;
} }
@ -348,7 +348,7 @@ class Worker
} }
if (!validate_include($include)) { if (!validate_include($include)) {
Logger::log("Include file ".$argv[0]." is not valid!"); Logger::warning("Include file is not valid", ['file' => $argv[0]]);
$stamp = (float)microtime(true); $stamp = (float)microtime(true);
DBA::delete('workerqueue', ['id' => $queue["id"]]); DBA::delete('workerqueue', ['id' => $queue["id"]]);
self::$db_duration = (microtime(true) - $stamp); self::$db_duration = (microtime(true) - $stamp);
@ -385,7 +385,7 @@ class Worker
self::$db_duration = (microtime(true) - $stamp); self::$db_duration = (microtime(true) - $stamp);
self::$db_duration_write += (microtime(true) - $stamp); self::$db_duration_write += (microtime(true) - $stamp);
} else { } else {
Logger::log("Function ".$funcname." does not exist"); Logger::warning("Function does not exist", ['function' => $funcname]);
$stamp = (float)microtime(true); $stamp = (float)microtime(true);
DBA::delete('workerqueue', ['id' => $queue["id"]]); DBA::delete('workerqueue', ['id' => $queue["id"]]);
self::$db_duration = (microtime(true) - $stamp); self::$db_duration = (microtime(true) - $stamp);
@ -533,7 +533,7 @@ class Worker
$level = ($used / $max) * 100; $level = ($used / $max) * 100;
if ($level >= $maxlevel) { if ($level >= $maxlevel) {
Logger::log("Maximum level (".$maxlevel."%) of user connections reached: ".$used."/".$max); Logger::notice("Maximum level (".$maxlevel."%) of user connections reached: ".$used."/".$max);
return true; return true;
} }
} }
@ -563,7 +563,7 @@ class Worker
if ($level < $maxlevel) { if ($level < $maxlevel) {
return false; return false;
} }
Logger::log("Maximum level (".$level."%) of system connections reached: ".$used."/".$max); Logger::notice("Maximum level (".$level."%) of system connections reached: ".$used."/".$max);
return true; return true;
} }
@ -615,7 +615,7 @@ class Worker
// How long is the process already running? // How long is the process already running?
$duration = (time() - strtotime($entry["executed"])) / 60; $duration = (time() - strtotime($entry["executed"])) / 60;
if ($duration > $max_duration) { if ($duration > $max_duration) {
Logger::log("Worker process ".$entry["pid"]." (".substr(json_encode($argv), 0, 50).") took more than ".$max_duration." minutes. It will be killed now."); Logger::notice("Worker process ".$entry["pid"]." (".substr(json_encode($argv), 0, 50).") took more than ".$max_duration." minutes. It will be killed now.");
posix_kill($entry["pid"], SIGTERM); posix_kill($entry["pid"], SIGTERM);
// We killed the stale process. // We killed the stale process.

View file

@ -669,10 +669,18 @@ class DBA
*/ */
public static function mergeConditions(array ...$conditions) public static function mergeConditions(array ...$conditions)
{ {
if (count($conditions) == 1) {
return current($conditions);
}
$conditionStrings = []; $conditionStrings = [];
$result = []; $result = [];
foreach ($conditions as $key => $condition) { foreach ($conditions as $key => $condition) {
if (!$condition) {
continue;
}
$condition = self::collapseCondition($condition); $condition = self::collapseCondition($condition);
$conditionStrings[] = array_shift($condition); $conditionStrings[] = array_shift($condition);

View file

@ -48,6 +48,22 @@ class DBStructure
*/ */
private static $definition = []; private static $definition = [];
/**
* Set a database version to trigger update functions
*
* @param string $version
* @return void
*/
public static function setDatabaseVersion(string $version)
{
if (!is_numeric($version)) {
throw new \Asika\SimpleConsole\CommandArgsException('The version number must be numeric');
}
DI::config()->set('system', 'build', $version);
echo DI::l10n()->t('The database version had been set to %s.', $version);
}
/** /**
* Converts all tables from MyISAM/InnoDB Antelope to InnoDB Barracuda * Converts all tables from MyISAM/InnoDB Antelope to InnoDB Barracuda
*/ */

View file

@ -23,9 +23,12 @@ namespace Friendica\Factory\Api\Mastodon;
use Friendica\App\BaseURL; use Friendica\App\BaseURL;
use Friendica\BaseFactory; use Friendica\BaseFactory;
use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\Verb;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Protocol\Activity;
use Friendica\Repository\ProfileField; use Friendica\Repository\ProfileField;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -59,6 +62,12 @@ class Status extends BaseFactory
$item = Item::selectFirst([], ['uri-id' => $uriId, 'uid' => $uid]); $item = Item::selectFirst([], ['uri-id' => $uriId, 'uid' => $uid]);
$account = DI::mstdnAccount()->createFromContactId($item['author-id']); $account = DI::mstdnAccount()->createFromContactId($item['author-id']);
return new \Friendica\Object\Api\Mastodon\Status($item, $account); $counts = new \Friendica\Object\Api\Mastodon\Status\Counts(
DBA::count('item', ['thr-parent-id' => $uriId, 'uid' => $uid, 'gravity' => GRAVITY_COMMENT]),
DBA::count('item', ['thr-parent-id' => $uriId, 'uid' => $uid, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::ANNOUNCE)]),
DBA::count('item', ['thr-parent-id' => $uriId, 'uid' => $uid, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::LIKE)])
);
return new \Friendica\Object\Api\Mastodon\Status($item, $account, $counts);
} }
} }

View file

@ -320,13 +320,18 @@ class APContact
$apcontact['updated'] = DateTimeFormat::utcNow(); $apcontact['updated'] = DateTimeFormat::utcNow();
DBA::update('apcontact', $apcontact, ['url' => $url], true);
// We delete the old entry when the URL is changed // We delete the old entry when the URL is changed
if (($url != $apcontact['url']) && DBA::exists('apcontact', ['url' => $url]) && DBA::exists('apcontact', ['url' => $apcontact['url']])) { if ($url != $apcontact['url']) {
Logger::info('Delete changed profile url', ['old' => $url, 'new' => $apcontact['url']]);
DBA::delete('apcontact', ['url' => $url]); DBA::delete('apcontact', ['url' => $url]);
} }
if (DBA::exists('apcontact', ['url' => $apcontact['url']])) {
DBA::update('apcontact', $apcontact, ['url' => $apcontact['url']]);
} else {
DBA::replace('apcontact', $apcontact);
}
Logger::info('Updated profile', ['url' => $url]); Logger::info('Updated profile', ['url' => $url]);
return $apcontact; return $apcontact;

View file

@ -1909,7 +1909,7 @@ class Contact
$ret['pubkey'] = $new_pubkey; $ret['pubkey'] = $new_pubkey;
} }
if (($ret['addr'] != $contact['addr']) || (!empty($ret['alias']) && ($ret['alias'] != $contact['alias']))) { if ((!empty($ret['addr']) && ($ret['addr'] != $contact['addr'])) || (!empty($ret['alias']) && ($ret['alias'] != $contact['alias']))) {
$ret['uri-date'] = DateTimeFormat::utcNow(); $ret['uri-date'] = DateTimeFormat::utcNow();
} }

View file

@ -58,6 +58,15 @@ class Item
const PT_DOCUMENT = 19; const PT_DOCUMENT = 19;
const PT_EVENT = 32; const PT_EVENT = 32;
const PT_TAG = 64; const PT_TAG = 64;
const PT_TO = 65;
const PT_CC = 66;
const PT_BTO = 67;
const PT_BCC = 68;
const PT_FOLLOWER = 69;
const PT_ANNOUNCEMENT = 70;
const PT_COMMENT = 71;
const PT_STORED = 72;
const PT_GLOBAL = 73;
const PT_PERSONAL_NOTE = 128; const PT_PERSONAL_NOTE = 128;
// Field list that is used to display the items // Field list that is used to display the items
@ -202,19 +211,7 @@ class Item
return []; return [];
} }
if (empty($condition) || !is_array($condition)) { $condition = DBA::mergeConditions(['iid' => $pinned], $condition);
$condition = ['iid' => $pinned];
} else {
reset($condition);
$first_key = key($condition);
if (!is_int($first_key)) {
$condition['iid'] = $pinned;
} else {
$values_string = substr(str_repeat("?, ", count($pinned)), 0, -2);
$condition[0] = '(' . $condition[0] . ") AND `iid` IN (" . $values_string . ")";
$condition = array_merge($condition, $pinned);
}
}
return self::selectThreadForUser($uid, $selected, $condition, $params); return self::selectThreadForUser($uid, $selected, $condition, $params);
} }
@ -1704,6 +1701,11 @@ class Item
'photo' => $item['owner-avatar'], 'network' => $item['network']]; 'photo' => $item['owner-avatar'], 'network' => $item['network']];
$item['owner-id'] = ($item['owner-id'] ?? 0) ?: Contact::getIdForURL($item['owner-link'], 0, null, $default); $item['owner-id'] = ($item['owner-id'] ?? 0) ?: Contact::getIdForURL($item['owner-link'], 0, null, $default);
$actor = ($item['gravity'] == GRAVITY_PARENT) ? $item['owner-id'] : $item['author-id'];
if (!$item['origin'] && in_array($item['post-type'], [self::PT_ARTICLE, self::PT_COMMENT, self::PT_GLOBAL]) && Contact::isSharing($actor, $item['uid'])) {
$item['post-type'] = self::PT_FOLLOWER;
}
// Ensure that there is an avatar cache // Ensure that there is an avatar cache
Contact::checkAvatarCache($item['author-id']); Contact::checkAvatarCache($item['author-id']);
Contact::checkAvatarCache($item['owner-id']); Contact::checkAvatarCache($item['owner-id']);
@ -2006,10 +2008,10 @@ class Item
*/ */
private static function setOwnerforResharedItem(array $item) private static function setOwnerforResharedItem(array $item)
{ {
$parent = self::selectFirst(['id', 'owner-id', 'author-id', 'author-link', 'origin'], $parent = self::selectFirst(['id', 'owner-id', 'author-id', 'author-link', 'origin', 'post-type'],
['uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]); ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']]);
if (!DBA::isResult($parent)) { if (!DBA::isResult($parent)) {
Logger::error('Parent not found', ['uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]); Logger::error('Parent not found', ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']]);
return; return;
} }
@ -2019,19 +2021,24 @@ class Item
return; return;
} }
if ($author['contact-type'] != Contact::TYPE_COMMUNITY) {
logger::info('The resharer is no forum: quit', ['resharer' => $item['author-id'], 'owner' => $parent['owner-id'], 'author' => $parent['author-id'], 'uid' => $item['uid']]);
return;
}
$cid = Contact::getIdForURL($author['url'], $item['uid']); $cid = Contact::getIdForURL($author['url'], $item['uid']);
if (empty($cid) || !Contact::isSharing($cid, $item['uid'])) { if (empty($cid) || !Contact::isSharing($cid, $item['uid'])) {
logger::info('The resharer is not a following contact: quit', ['resharer' => $author['url'], 'uid' => $item['uid']]); Logger::info('The resharer is not a following contact: quit', ['resharer' => $author['url'], 'uid' => $item['uid']]);
return; return;
} }
Item::update(['owner-id' => $item['author-id'], 'contact-id' => $cid], ['id' => $parent['id']]); if ($author['contact-type'] != Contact::TYPE_COMMUNITY) {
Logger::info('Change owner of the parent', ['uri-id' => $item['uri-id'], 'parent-uri-id' => $item['parent-uri-id'], 'uid' => $item['uid'], 'owner-id' => $item['author-id'], 'contact-id' => $cid]); if (!in_array($parent['post-type'], [self::PT_ARTICLE, self::PT_COMMENT]) || Contact::isSharing($parent['owner-id'], $item['uid'])) {
Logger::info('The resharer is no forum: quit', ['resharer' => $item['author-id'], 'owner' => $parent['owner-id'], 'author' => $parent['author-id'], 'uid' => $item['uid']]);
return;
}
self::update(['post-type' => self::PT_ANNOUNCEMENT], ['id' => $parent['id']]);
Logger::info('Set announcement post-type', ['uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid']]);
return;
}
self::update(['owner-id' => $item['author-id'], 'contact-id' => $cid], ['id' => $parent['id']]);
Logger::info('Change owner of the parent', ['uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid'], 'owner-id' => $item['author-id'], 'contact-id' => $cid]);
} }
/** /**
@ -2237,6 +2244,8 @@ class Item
return 0; return 0;
} }
$item['post-type'] = self::PT_STORED;
$item = array_merge($item, $fields); $item = array_merge($item, $fields);
$stored = self::storeForUser($item, $uid); $stored = self::storeForUser($item, $uid);

View file

@ -24,10 +24,39 @@ namespace Friendica\Model;
use Friendica\Content\Text; use Friendica\Content\Text;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
class ItemContent class ItemContent
{ {
public static function getURIIdListBySearch(string $search, int $uid = 0, int $start = 0, int $limit = 100)
{
$condition = ["`uri-id` IN (SELECT `uri-id` FROM `item-content` WHERE MATCH (`title`, `content-warning`, `body`) AGAINST (? IN BOOLEAN MODE))
AND (NOT `private` OR (`private` AND `uid` = ?))", $search, $uid];
$params = [
'order' => ['uri-id' => true],
'group_by' => ['uri-id'],
'limit' => [$start, $limit]
];
$tags = DBA::select('item', ['uri-id'], $condition, $params);
$uriids = [];
while ($tag = DBA::fetch($tags)) {
$uriids[] = $tag['uri-id'];
}
DBA::close($tags);
return $uriids;
}
public static function countBySearch(string $search, int $uid = 0)
{
$condition = ["`uri-id` IN (SELECT `uri-id` FROM `item-content` WHERE MATCH (`title`, `content-warning`, `body`) AGAINST (? IN BOOLEAN MODE))
AND (NOT `private` OR (`private` AND `uid` = ?))", $search, $uid];
return DBA::count('item', $condition);
}
/** /**
* Convert a message into plaintext for connectors to other networks * Convert a message into plaintext for connectors to other networks
* *

View file

@ -129,9 +129,12 @@ class Mail
} }
$me = DBA::selectFirst('contact', [], ['uid' => local_user(), 'self' => true]); $me = DBA::selectFirst('contact', [], ['uid' => local_user(), 'self' => true]);
$contact = DBA::selectFirst('contact', [], ['id' => $recipient, 'uid' => local_user()]); if (!DBA::isResult($me)) {
return -2;
}
if (!(count($me) && (count($contact)))) { $contact = DBA::selectFirst('contact', [], ['id' => $recipient, 'uid' => local_user()]);
if (!DBA::isResult($contact)) {
return -2; return -2;
} }

View file

@ -304,7 +304,7 @@ class Profile
$profile_is_dfrn = $profile['network'] == Protocol::DFRN; $profile_is_dfrn = $profile['network'] == Protocol::DFRN;
$profile_is_native = in_array($profile['network'], Protocol::NATIVE_SUPPORT); $profile_is_native = in_array($profile['network'], Protocol::NATIVE_SUPPORT);
$local_user_is_self = local_user() && local_user() == ($profile['uid'] ?? 0); $local_user_is_self = $profile['self'] ?? false;
$visitor_is_authenticated = (bool)self::getMyURL(); $visitor_is_authenticated = (bool)self::getMyURL();
$visitor_is_following = $visitor_is_following =
in_array($visitor_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND]) in_array($visitor_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND])
@ -354,13 +354,7 @@ class Profile
// Fetch the account type // Fetch the account type
$account_type = Contact::getAccountType($profile); $account_type = Contact::getAccountType($profile);
if (!empty($profile['address']) if (!empty($profile['address']) || !empty($profile['location'])) {
|| !empty($profile['location'])
|| !empty($profile['locality'])
|| !empty($profile['region'])
|| !empty($profile['postal-code'])
|| !empty($profile['country-name'])
) {
$location = DI::l10n()->t('Location:'); $location = DI::l10n()->t('Location:');
} }
@ -427,10 +421,6 @@ class Profile
$p['about'] = BBCode::convert($p['about']); $p['about'] = BBCode::convert($p['about']);
} }
if (empty($p['address']) && !empty($p['location'])) {
$p['address'] = $p['location'];
}
if (isset($p['address'])) { if (isset($p['address'])) {
$p['address'] = BBCode::convert($p['address']); $p['address'] = BBCode::convert($p['address']);
} }

View file

@ -440,6 +440,21 @@ class Tag
return $return; return $return;
} }
/**
* Counts posts for given tag
*
* @param string $search
* @param integer $uid
* @return integer number of posts
*/
public static function countByTag(string $search, int $uid = 0)
{
$condition = ["`name` = ? AND (NOT `private` OR (`private` AND `uid` = ?))", $search, $uid];
$params = ['group_by' => ['uri-id']];
return DBA::count('tag-search-view', $condition, $params);
}
/** /**
* Search posts for given tag * Search posts for given tag
* *

View file

@ -51,8 +51,8 @@ class UserItem
*/ */
public static function setNotification(int $iid) public static function setNotification(int $iid)
{ {
$fields = ['id', 'uri-id', 'uid', 'body', 'parent', 'gravity', 'tag', $fields = ['id', 'uri-id', 'parent-uri-id', 'uid', 'body', 'parent', 'gravity', 'tag',
'contact-id', 'thr-parent', 'parent-uri', 'author-id', 'verb']; 'private', 'contact-id', 'thr-parent', 'parent-uri', 'author-id', 'verb'];
$item = Item::selectFirst($fields, ['id' => $iid, 'origin' => false]); $item = Item::selectFirst($fields, ['id' => $iid, 'origin' => false]);
if (!DBA::isResult($item)) { if (!DBA::isResult($item)) {
return; return;
@ -63,14 +63,26 @@ class UserItem
return; return;
} }
// fetch all users in the thread if ($item['uid'] == 0) {
$uids = [];
} else {
// Always include the item user
$uids = [$item['uid']];
}
// Add every user who participated so far in this thread
// This can only happen with participations on global items. (means: uid = 0)
$users = DBA::p("SELECT DISTINCT(`contact`.`uid`) FROM `item` $users = DBA::p("SELECT DISTINCT(`contact`.`uid`) FROM `item`
INNER JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` != 0 INNER JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` != 0
WHERE `parent` IN (SELECT `parent` FROM `item` WHERE `id`=?)", $iid); WHERE `parent` IN (SELECT `parent` FROM `item` WHERE `id`=?)", $iid);
while ($user = DBA::fetch($users)) { while ($user = DBA::fetch($users)) {
self::setNotificationForUser($item, $user['uid']); $uids[] = $user['uid'];
} }
DBA::close($users); DBA::close($users);
foreach (array_unique($uids) as $uid) {
self::setNotificationForUser($item, $uid);
}
} }
/** /**

View file

@ -44,7 +44,7 @@ class Contact extends BaseAdmin
$contact_id = Model\Contact::getIdForURL($contact_url); $contact_id = Model\Contact::getIdForURL($contact_url);
if ($contact_id) { if ($contact_id) {
Model\Contact::block($contact_id, $block_reason); Model\Contact::block($contact_id, $block_reason);
notice(DI::l10n()->t('The contact has been blocked from the node')); info(DI::l10n()->t('The contact has been blocked from the node'));
} else { } else {
notice(DI::l10n()->t('Could not find any contact entry for this URL (%s)', $contact_url)); notice(DI::l10n()->t('Could not find any contact entry for this URL (%s)', $contact_url));
} }
@ -54,7 +54,7 @@ class Contact extends BaseAdmin
foreach ($contacts as $uid) { foreach ($contacts as $uid) {
Model\Contact::unblock($uid); Model\Contact::unblock($uid);
} }
notice(DI::l10n()->tt('%s contact unblocked', '%s contacts unblocked', count($contacts))); info(DI::l10n()->tt('%s contact unblocked', '%s contacts unblocked', count($contacts)));
} }
DI::baseUrl()->redirect('admin/blocklist/contact'); DI::baseUrl()->redirect('admin/blocklist/contact');

View file

@ -542,12 +542,6 @@ class Site extends BaseAdmin
$diaspora_able = (DI::baseUrl()->getUrlPath() == ''); $diaspora_able = (DI::baseUrl()->getUrlPath() == '');
$optimize_max_tablesize = DI::config()->get('system', 'optimize_max_tablesize', -1);
if ($optimize_max_tablesize <= 0) {
$optimize_max_tablesize = -1;
}
$current_storage_backend = DI::storage(); $current_storage_backend = DI::storage();
$available_storage_backends = []; $available_storage_backends = [];
@ -634,12 +628,12 @@ class Site extends BaseAdmin
'$allowed_oembed' => ['allowed_oembed', DI::l10n()->t('Allowed OEmbed domains'), DI::config()->get('system', 'allowed_oembed'), DI::l10n()->t('Comma separated list of domains which oembed content is allowed to be displayed. Wildcards are accepted.')], '$allowed_oembed' => ['allowed_oembed', DI::l10n()->t('Allowed OEmbed domains'), DI::config()->get('system', 'allowed_oembed'), DI::l10n()->t('Comma separated list of domains which oembed content is allowed to be displayed. Wildcards are accepted.')],
'$block_public' => ['block_public', DI::l10n()->t('Block public'), DI::config()->get('system', 'block_public'), DI::l10n()->t('Check to block public access to all otherwise public personal pages on this site unless you are currently logged in.')], '$block_public' => ['block_public', DI::l10n()->t('Block public'), DI::config()->get('system', 'block_public'), DI::l10n()->t('Check to block public access to all otherwise public personal pages on this site unless you are currently logged in.')],
'$force_publish' => ['publish_all', DI::l10n()->t('Force publish'), DI::config()->get('system', 'publish_all'), DI::l10n()->t('Check to force all profiles on this site to be listed in the site directory.') . '<strong>' . DI::l10n()->t('Enabling this may violate privacy laws like the GDPR') . '</strong>'], '$force_publish' => ['publish_all', DI::l10n()->t('Force publish'), DI::config()->get('system', 'publish_all'), DI::l10n()->t('Check to force all profiles on this site to be listed in the site directory.') . '<strong>' . DI::l10n()->t('Enabling this may violate privacy laws like the GDPR') . '</strong>'],
'$global_directory' => ['directory', DI::l10n()->t('Global directory URL'), DI::config()->get('system', 'directory', 'https://dir.friendica.social'), DI::l10n()->t('URL to the global directory. If this is not set, the global directory is completely unavailable to the application.')], '$global_directory' => ['directory', DI::l10n()->t('Global directory URL'), DI::config()->get('system', 'directory'), DI::l10n()->t('URL to the global directory. If this is not set, the global directory is completely unavailable to the application.')],
'$newuser_private' => ['newuser_private', DI::l10n()->t('Private posts by default for new users'), DI::config()->get('system', 'newuser_private'), DI::l10n()->t('Set default post permissions for all new members to the default privacy group rather than public.')], '$newuser_private' => ['newuser_private', DI::l10n()->t('Private posts by default for new users'), DI::config()->get('system', 'newuser_private'), DI::l10n()->t('Set default post permissions for all new members to the default privacy group rather than public.')],
'$enotify_no_content' => ['enotify_no_content', DI::l10n()->t('Don\'t include post content in email notifications'), DI::config()->get('system', 'enotify_no_content'), DI::l10n()->t('Don\'t include the content of a post/comment/private message/etc. in the email notifications that are sent out from this site, as a privacy measure.')], '$enotify_no_content' => ['enotify_no_content', DI::l10n()->t('Don\'t include post content in email notifications'), DI::config()->get('system', 'enotify_no_content'), DI::l10n()->t('Don\'t include the content of a post/comment/private message/etc. in the email notifications that are sent out from this site, as a privacy measure.')],
'$private_addons' => ['private_addons', DI::l10n()->t('Disallow public access to addons listed in the apps menu.'), DI::config()->get('config', 'private_addons'), DI::l10n()->t('Checking this box will restrict addons listed in the apps menu to members only.')], '$private_addons' => ['private_addons', DI::l10n()->t('Disallow public access to addons listed in the apps menu.'), DI::config()->get('config', 'private_addons'), DI::l10n()->t('Checking this box will restrict addons listed in the apps menu to members only.')],
'$disable_embedded' => ['disable_embedded', DI::l10n()->t('Don\'t embed private images in posts'), DI::config()->get('system', 'disable_embedded'), DI::l10n()->t('Don\'t replace locally-hosted private photos in posts with an embedded copy of the image. This means that contacts who receive posts containing private photos will have to authenticate and load each image, which may take a while.')], '$disable_embedded' => ['disable_embedded', DI::l10n()->t('Don\'t embed private images in posts'), DI::config()->get('system', 'disable_embedded'), DI::l10n()->t('Don\'t replace locally-hosted private photos in posts with an embedded copy of the image. This means that contacts who receive posts containing private photos will have to authenticate and load each image, which may take a while.')],
'$explicit_content' => ['explicit_content', DI::l10n()->t('Explicit Content'), DI::config()->get('system', 'explicit_content', false), DI::l10n()->t('Set this to announce that your node is used mostly for explicit content that might not be suited for minors. This information will be published in the node information and might be used, e.g. by the global directory, to filter your node from listings of nodes to join. Additionally a note about this will be shown at the user registration page.')], '$explicit_content' => ['explicit_content', DI::l10n()->t('Explicit Content'), DI::config()->get('system', 'explicit_content'), DI::l10n()->t('Set this to announce that your node is used mostly for explicit content that might not be suited for minors. This information will be published in the node information and might be used, e.g. by the global directory, to filter your node from listings of nodes to join. Additionally a note about this will be shown at the user registration page.')],
'$allow_users_remote_self'=> ['allow_users_remote_self', DI::l10n()->t('Allow Users to set remote_self'), DI::config()->get('system', 'allow_users_remote_self'), DI::l10n()->t('With checking this, every user is allowed to mark every contact as a remote_self in the repair contact dialog. Setting this flag on a contact causes mirroring every posting of that contact in the users stream.')], '$allow_users_remote_self'=> ['allow_users_remote_self', DI::l10n()->t('Allow Users to set remote_self'), DI::config()->get('system', 'allow_users_remote_self'), DI::l10n()->t('With checking this, every user is allowed to mark every contact as a remote_self in the repair contact dialog. Setting this flag on a contact causes mirroring every posting of that contact in the users stream.')],
'$no_multi_reg' => ['no_multi_reg', DI::l10n()->t('Block multiple registrations'), DI::config()->get('system', 'block_extended_register'), DI::l10n()->t('Disallow users to register additional accounts for use as pages.')], '$no_multi_reg' => ['no_multi_reg', DI::l10n()->t('Block multiple registrations'), DI::config()->get('system', 'block_extended_register'), DI::l10n()->t('Disallow users to register additional accounts for use as pages.')],
'$no_openid' => ['no_openid', DI::l10n()->t('Disable OpenID'), DI::config()->get('system', 'no_openid'), DI::l10n()->t('Disable OpenID support for registration and logins.')], '$no_openid' => ['no_openid', DI::l10n()->t('Disable OpenID'), DI::config()->get('system', 'no_openid'), DI::l10n()->t('Disable OpenID support for registration and logins.')],
@ -655,11 +649,11 @@ class Site extends BaseAdmin
'$verifyssl' => ['verifyssl', DI::l10n()->t('Verify SSL'), DI::config()->get('system', 'verifyssl'), DI::l10n()->t('If you wish, you can turn on strict certificate checking. This will mean you cannot connect (at all) to self-signed SSL sites.')], '$verifyssl' => ['verifyssl', DI::l10n()->t('Verify SSL'), DI::config()->get('system', 'verifyssl'), DI::l10n()->t('If you wish, you can turn on strict certificate checking. This will mean you cannot connect (at all) to self-signed SSL sites.')],
'$proxyuser' => ['proxyuser', DI::l10n()->t('Proxy user'), DI::config()->get('system', 'proxyuser'), ''], '$proxyuser' => ['proxyuser', DI::l10n()->t('Proxy user'), DI::config()->get('system', 'proxyuser'), ''],
'$proxy' => ['proxy', DI::l10n()->t('Proxy URL'), DI::config()->get('system', 'proxy'), ''], '$proxy' => ['proxy', DI::l10n()->t('Proxy URL'), DI::config()->get('system', 'proxy'), ''],
'$timeout' => ['timeout', DI::l10n()->t('Network timeout'), DI::config()->get('system', 'curl_timeout', 60), DI::l10n()->t('Value is in seconds. Set to 0 for unlimited (not recommended).')], '$timeout' => ['timeout', DI::l10n()->t('Network timeout'), DI::config()->get('system', 'curl_timeout'), DI::l10n()->t('Value is in seconds. Set to 0 for unlimited (not recommended).')],
'$maxloadavg' => ['maxloadavg', DI::l10n()->t('Maximum Load Average'), DI::config()->get('system', 'maxloadavg', 20), DI::l10n()->t('Maximum system load before delivery and poll processes are deferred - default %d.', 20)], '$maxloadavg' => ['maxloadavg', DI::l10n()->t('Maximum Load Average'), DI::config()->get('system', 'maxloadavg'), DI::l10n()->t('Maximum system load before delivery and poll processes are deferred - default %d.', 20)],
'$maxloadavg_frontend' => ['maxloadavg_frontend', DI::l10n()->t('Maximum Load Average (Frontend)'), DI::config()->get('system', 'maxloadavg_frontend', 50), DI::l10n()->t('Maximum system load before the frontend quits service - default 50.')], '$maxloadavg_frontend' => ['maxloadavg_frontend', DI::l10n()->t('Maximum Load Average (Frontend)'), DI::config()->get('system', 'maxloadavg_frontend'), DI::l10n()->t('Maximum system load before the frontend quits service - default 50.')],
'$min_memory' => ['min_memory', DI::l10n()->t('Minimal Memory'), DI::config()->get('system', 'min_memory', 0), DI::l10n()->t('Minimal free memory in MB for the worker. Needs access to /proc/meminfo - default 0 (deactivated).')], '$min_memory' => ['min_memory', DI::l10n()->t('Minimal Memory'), DI::config()->get('system', 'min_memory'), DI::l10n()->t('Minimal free memory in MB for the worker. Needs access to /proc/meminfo - default 0 (deactivated).')],
'$optimize_tables' => ['optimize_tables', DI::l10n()->t('Periodically optimize tables'), DI::config()->get('system', 'optimize_tables', false), DI::l10n()->t('Periodically optimize tables like the cache and the workerqueue')], '$optimize_tables' => ['optimize_tables', DI::l10n()->t('Periodically optimize tables'), DI::config()->get('system', 'optimize_tables'), DI::l10n()->t('Periodically optimize tables like the cache and the workerqueue')],
'$contact_discovery' => ['contact_discovery', DI::l10n()->t('Discover followers/followings from contacts'), DI::config()->get('system', 'contact_discovery'), DI::l10n()->t('If enabled, contacts are checked for their followers and following contacts.') . '<ul>' . '$contact_discovery' => ['contact_discovery', DI::l10n()->t('Discover followers/followings from contacts'), DI::config()->get('system', 'contact_discovery'), DI::l10n()->t('If enabled, contacts are checked for their followers and following contacts.') . '<ul>' .
'<li>' . DI::l10n()->t('None - deactivated') . '</li>' . '<li>' . DI::l10n()->t('None - deactivated') . '</li>' .
@ -676,10 +670,10 @@ class Site extends BaseAdmin
'$check_new_version_url' => ['check_new_version_url', DI::l10n()->t('Check upstream version'), DI::config()->get('system', 'check_new_version_url'), DI::l10n()->t('Enables checking for new Friendica versions at github. If there is a new version, you will be informed in the admin panel overview.'), $check_git_version_choices], '$check_new_version_url' => ['check_new_version_url', DI::l10n()->t('Check upstream version'), DI::config()->get('system', 'check_new_version_url'), DI::l10n()->t('Enables checking for new Friendica versions at github. If there is a new version, you will be informed in the admin panel overview.'), $check_git_version_choices],
'$suppress_tags' => ['suppress_tags', DI::l10n()->t('Suppress Tags'), DI::config()->get('system', 'suppress_tags'), DI::l10n()->t('Suppress showing a list of hashtags at the end of the posting.')], '$suppress_tags' => ['suppress_tags', DI::l10n()->t('Suppress Tags'), DI::config()->get('system', 'suppress_tags'), DI::l10n()->t('Suppress showing a list of hashtags at the end of the posting.')],
'$dbclean' => ['dbclean', DI::l10n()->t('Clean database'), DI::config()->get('system', 'dbclean', false), DI::l10n()->t('Remove old remote items, orphaned database records and old content from some other helper tables.')], '$dbclean' => ['dbclean', DI::l10n()->t('Clean database'), DI::config()->get('system', 'dbclean'), DI::l10n()->t('Remove old remote items, orphaned database records and old content from some other helper tables.')],
'$dbclean_expire_days' => ['dbclean_expire_days', DI::l10n()->t('Lifespan of remote items'), DI::config()->get('system', 'dbclean-expire-days', 0), DI::l10n()->t('When the database cleanup is enabled, this defines the days after which remote items will be deleted. Own items, and marked or filed items are always kept. 0 disables this behaviour.')], '$dbclean_expire_days' => ['dbclean_expire_days', DI::l10n()->t('Lifespan of remote items'), DI::config()->get('system', 'dbclean-expire-days'), DI::l10n()->t('When the database cleanup is enabled, this defines the days after which remote items will be deleted. Own items, and marked or filed items are always kept. 0 disables this behaviour.')],
'$dbclean_unclaimed' => ['dbclean_unclaimed', DI::l10n()->t('Lifespan of unclaimed items'), DI::config()->get('system', 'dbclean-expire-unclaimed', 90), DI::l10n()->t('When the database cleanup is enabled, this defines the days after which unclaimed remote items (mostly content from the relay) will be deleted. Default value is 90 days. Defaults to the general lifespan value of remote items if set to 0.')], '$dbclean_unclaimed' => ['dbclean_unclaimed', DI::l10n()->t('Lifespan of unclaimed items'), DI::config()->get('system', 'dbclean-expire-unclaimed'), DI::l10n()->t('When the database cleanup is enabled, this defines the days after which unclaimed remote items (mostly content from the relay) will be deleted. Default value is 90 days. Defaults to the general lifespan value of remote items if set to 0.')],
'$dbclean_expire_conv' => ['dbclean_expire_conv', DI::l10n()->t('Lifespan of raw conversation data'), DI::config()->get('system', 'dbclean_expire_conversation', 90), DI::l10n()->t('The conversation data is used for ActivityPub and OStatus, as well as for debug purposes. It should be safe to remove it after 14 days, default is 90 days.')], '$dbclean_expire_conv' => ['dbclean_expire_conv', DI::l10n()->t('Lifespan of raw conversation data'), DI::config()->get('system', 'dbclean_expire_conversation'), DI::l10n()->t('The conversation data is used for ActivityPub and OStatus, as well as for debug purposes. It should be safe to remove it after 14 days, default is 90 days.')],
'$itemcache' => ['itemcache', DI::l10n()->t('Path to item cache'), DI::config()->get('system', 'itemcache'), DI::l10n()->t('The item caches buffers generated bbcode and external images.')], '$itemcache' => ['itemcache', DI::l10n()->t('Path to item cache'), DI::config()->get('system', 'itemcache'), DI::l10n()->t('The item caches buffers generated bbcode and external images.')],
'$itemcache_duration' => ['itemcache_duration', DI::l10n()->t('Cache duration in seconds'), DI::config()->get('system', 'itemcache_duration'), DI::l10n()->t('How long should the cache files be hold? Default value is 86400 seconds (One day). To disable the item cache, set the value to -1.')], '$itemcache_duration' => ['itemcache_duration', DI::l10n()->t('Cache duration in seconds'), DI::config()->get('system', 'itemcache_duration'), DI::l10n()->t('How long should the cache files be hold? Default value is 86400 seconds (One day). To disable the item cache, set the value to -1.')],
'$max_comments' => ['max_comments', DI::l10n()->t('Maximum numbers of comments per post'), DI::config()->get('system', 'max_comments'), DI::l10n()->t('How much comments should be shown for each post? Default value is 100.')], '$max_comments' => ['max_comments', DI::l10n()->t('Maximum numbers of comments per post'), DI::config()->get('system', 'max_comments'), DI::l10n()->t('How much comments should be shown for each post? Default value is 100.')],
@ -698,11 +692,11 @@ class Site extends BaseAdmin
'$worker_frontend' => ['worker_frontend', DI::l10n()->t('Enable frontend worker'), DI::config()->get('system', 'frontend_worker'), DI::l10n()->t('When enabled the Worker process is triggered when backend access is performed (e.g. messages being delivered). On smaller sites you might want to call %s/worker on a regular basis via an external cron job. You should only enable this option if you cannot utilize cron/scheduled jobs on your server.', DI::baseUrl()->get())], '$worker_frontend' => ['worker_frontend', DI::l10n()->t('Enable frontend worker'), DI::config()->get('system', 'frontend_worker'), DI::l10n()->t('When enabled the Worker process is triggered when backend access is performed (e.g. messages being delivered). On smaller sites you might want to call %s/worker on a regular basis via an external cron job. You should only enable this option if you cannot utilize cron/scheduled jobs on your server.', DI::baseUrl()->get())],
'$relay_subscribe' => ['relay_subscribe', DI::l10n()->t('Subscribe to relay'), DI::config()->get('system', 'relay_subscribe'), DI::l10n()->t('Enables the receiving of public posts from the relay. They will be included in the search, subscribed tags and on the global community page.')], '$relay_subscribe' => ['relay_subscribe', DI::l10n()->t('Subscribe to relay'), DI::config()->get('system', 'relay_subscribe'), DI::l10n()->t('Enables the receiving of public posts from the relay. They will be included in the search, subscribed tags and on the global community page.')],
'$relay_server' => ['relay_server', DI::l10n()->t('Relay server'), DI::config()->get('system', 'relay_server', 'https://relay.diasp.org'), DI::l10n()->t('Address of the relay server where public posts should be send to. For example https://relay.diasp.org')], '$relay_server' => ['relay_server', DI::l10n()->t('Relay server'), DI::config()->get('system', 'relay_server'), DI::l10n()->t('Address of the relay server where public posts should be send to. For example %s', 'https://social-relay.isurf.ca')],
'$relay_directly' => ['relay_directly', DI::l10n()->t('Direct relay transfer'), DI::config()->get('system', 'relay_directly'), DI::l10n()->t('Enables the direct transfer to other servers without using the relay servers')], '$relay_directly' => ['relay_directly', DI::l10n()->t('Direct relay transfer'), DI::config()->get('system', 'relay_directly'), DI::l10n()->t('Enables the direct transfer to other servers without using the relay servers')],
'$relay_scope' => ['relay_scope', DI::l10n()->t('Relay scope'), DI::config()->get('system', 'relay_scope'), DI::l10n()->t('Can be "all" or "tags". "all" means that every public post should be received. "tags" means that only posts with selected tags should be received.'), ['' => DI::l10n()->t('Disabled'), 'all' => DI::l10n()->t('all'), 'tags' => DI::l10n()->t('tags')]], '$relay_scope' => ['relay_scope', DI::l10n()->t('Relay scope'), DI::config()->get('system', 'relay_scope'), DI::l10n()->t('Can be "all" or "tags". "all" means that every public post should be received. "tags" means that only posts with selected tags should be received.'), ['' => DI::l10n()->t('Disabled'), 'all' => DI::l10n()->t('all'), 'tags' => DI::l10n()->t('tags')]],
'$relay_server_tags' => ['relay_server_tags', DI::l10n()->t('Server tags'), DI::config()->get('system', 'relay_server_tags'), DI::l10n()->t('Comma separated list of tags for the "tags" subscription.')], '$relay_server_tags' => ['relay_server_tags', DI::l10n()->t('Server tags'), DI::config()->get('system', 'relay_server_tags'), DI::l10n()->t('Comma separated list of tags for the "tags" subscription.')],
'$relay_user_tags' => ['relay_user_tags', DI::l10n()->t('Allow user tags'), DI::config()->get('system', 'relay_user_tags', true), DI::l10n()->t('If enabled, the tags from the saved searches will used for the "tags" subscription in addition to the "relay_server_tags".')], '$relay_user_tags' => ['relay_user_tags', DI::l10n()->t('Allow user tags'), DI::config()->get('system', 'relay_user_tags'), DI::l10n()->t('If enabled, the tags from the saved searches will used for the "tags" subscription in addition to the "relay_server_tags".')],
'$form_security_token' => self::getFormSecurityToken('admin_site'), '$form_security_token' => self::getFormSecurityToken('admin_site'),
'$relocate_button' => DI::l10n()->t('Start Relocation'), '$relocate_button' => DI::l10n()->t('Start Relocation'),

View file

@ -58,14 +58,14 @@ class Users extends BaseAdmin
foreach ($users as $uid) { foreach ($users as $uid) {
User::block($uid); User::block($uid);
} }
notice(DI::l10n()->tt('%s user blocked', '%s users blocked', count($users))); info(DI::l10n()->tt('%s user blocked', '%s users blocked', count($users)));
} }
if (!empty($_POST['page_users_unblock'])) { if (!empty($_POST['page_users_unblock'])) {
foreach ($users as $uid) { foreach ($users as $uid) {
User::block($uid, false); User::block($uid, false);
} }
notice(DI::l10n()->tt('%s user unblocked', '%s users unblocked', count($users))); info(DI::l10n()->tt('%s user unblocked', '%s users unblocked', count($users)));
} }
if (!empty($_POST['page_users_delete'])) { if (!empty($_POST['page_users_delete'])) {
@ -77,21 +77,21 @@ class Users extends BaseAdmin
} }
} }
notice(DI::l10n()->tt('%s user deleted', '%s users deleted', count($users))); info(DI::l10n()->tt('%s user deleted', '%s users deleted', count($users)));
} }
if (!empty($_POST['page_users_approve'])) { if (!empty($_POST['page_users_approve'])) {
foreach ($pending as $hash) { foreach ($pending as $hash) {
User::allow($hash); User::allow($hash);
} }
notice(DI::l10n()->tt('%s user approved', '%s users approved', count($pending))); info(DI::l10n()->tt('%s user approved', '%s users approved', count($pending)));
} }
if (!empty($_POST['page_users_deny'])) { if (!empty($_POST['page_users_deny'])) {
foreach ($pending as $hash) { foreach ($pending as $hash) {
User::deny($hash); User::deny($hash);
} }
notice(DI::l10n()->tt('%s registration revoked', '%s registrations revoked', count($pending))); info(DI::l10n()->tt('%s registration revoked', '%s registrations revoked', count($pending)));
} }
DI::baseUrl()->redirect('admin/users'); DI::baseUrl()->redirect('admin/users');

View file

@ -57,11 +57,11 @@ class PublicTimeline extends BaseApi
$params = ['order' => ['uri-id' => true], 'limit' => $limit]; $params = ['order' => ['uri-id' => true], 'limit' => $limit];
$condition = ['gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'private' => Item::PUBLIC, 'network' => Protocol::FEDERATED]; $condition = ['gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'private' => Item::PUBLIC,
'uid' => 0, 'network' => Protocol::FEDERATED];
if ($local) { if ($local) {
$condition['origin'] = true; $condition = DBA::mergeConditions($condition, ["`uri-id` IN (SELECT `uri-id` FROM `item` WHERE `origin`)"]);
} else {
$condition['uid'] = 0;
} }
if ($remote) { if ($remote) {

View file

@ -73,7 +73,7 @@ abstract class ContactEndpoint extends BaseApi
throw new HTTPException\NotFoundException(DI::l10n()->t('User not found')); throw new HTTPException\NotFoundException(DI::l10n()->t('User not found'));
} }
$uid = $user['uid']; $uid = (int)$user['uid'];
} }
return $uid; return $uid;
@ -111,7 +111,7 @@ abstract class ContactEndpoint extends BaseApi
'next_cursor_str' => $return['next_cursor_str'], 'next_cursor_str' => $return['next_cursor_str'],
'previous_cursor' => $return['previous_cursor'], 'previous_cursor' => $return['previous_cursor'],
'previous_cursor_str' => $return['previous_cursor_str'], 'previous_cursor_str' => $return['previous_cursor_str'],
'total_count' => $return['total_count'], 'total_count' => (int)$return['total_count'],
]; ];
return $return; return $return;
@ -153,7 +153,7 @@ abstract class ContactEndpoint extends BaseApi
'pending' => false 'pending' => false
]; ];
$total_count = DBA::count('contact', $condition); $total_count = (int)DBA::count('contact', $condition);
if ($cursor !== -1) { if ($cursor !== -1) {
if ($cursor > 0) { if ($cursor > 0) {
@ -171,7 +171,7 @@ abstract class ContactEndpoint extends BaseApi
// Cursor is on the user-specific contact id since it's the sort field // Cursor is on the user-specific contact id since it's the sort field
if (count($ids)) { if (count($ids)) {
$previous_cursor = -$ids[0]; $previous_cursor = -$ids[0];
$next_cursor = $ids[count($ids) -1]; $next_cursor = (int)$ids[count($ids) -1];
} }
// No next page // No next page

View file

@ -64,7 +64,7 @@ abstract class BaseAdmin extends BaseModule
} }
if (!empty($_SESSION['submanage'])) { if (!empty($_SESSION['submanage'])) {
throw new HTTPException\ForbiddenException(DI::l10n()->t('Submanaged account can\'t access the administation pages. Please log back in as the main account.')); throw new HTTPException\ForbiddenException(DI::l10n()->t('Submanaged account can\'t access the administration pages. Please log back in as the main account.'));
} }
} }

View file

@ -42,13 +42,13 @@ class BaseApi extends BaseModule
{ {
$arguments = DI::args(); $arguments = DI::args();
if (substr($arguments->getQueryString(), -4) === '.xml') { if (substr($arguments->getCommand(), -4) === '.xml') {
self::$format = 'xml'; self::$format = 'xml';
} }
if (substr($arguments->getQueryString(), -4) === '.rss') { if (substr($arguments->getCommand(), -4) === '.rss') {
self::$format = 'rss'; self::$format = 'rss';
} }
if (substr($arguments->getQueryString(), -4) === '.atom') { if (substr($arguments->getCommand(), -4) === '.atom') {
self::$format = 'atom'; self::$format = 'atom';
} }
} }

View file

@ -436,17 +436,6 @@ class Contact extends BaseModule
if ($cmd === 'drop' && ($orig_record['uid'] != 0)) { if ($cmd === 'drop' && ($orig_record['uid'] != 0)) {
// Check if we should do HTML-based delete confirmation // Check if we should do HTML-based delete confirmation
if (!empty($_REQUEST['confirm'])) { if (!empty($_REQUEST['confirm'])) {
// <form> can't take arguments in its 'action' parameter
// so add any arguments as hidden inputs
$query = explode_querystring(DI::args()->getQueryString());
$inputs = [];
foreach ($query['args'] as $arg) {
if (strpos($arg, 'confirm=') === false) {
$arg_parts = explode('=', $arg);
$inputs[] = ['name' => $arg_parts[0], 'value' => $arg_parts[1]];
}
}
DI::page()['aside'] = ''; DI::page()['aside'] = '';
return Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_drop_confirm.tpl'), [ return Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_drop_confirm.tpl'), [
@ -454,9 +443,8 @@ class Contact extends BaseModule
'$contact' => self::getContactTemplateVars($orig_record), '$contact' => self::getContactTemplateVars($orig_record),
'$method' => 'get', '$method' => 'get',
'$message' => DI::l10n()->t('Do you really want to delete this contact?'), '$message' => DI::l10n()->t('Do you really want to delete this contact?'),
'$extra_inputs' => $inputs,
'$confirm' => DI::l10n()->t('Yes'), '$confirm' => DI::l10n()->t('Yes'),
'$confirm_url' => $query['base'], '$confirm_url' => DI::args()->getCommand(),
'$confirm_name' => 'confirmed', '$confirm_name' => 'confirmed',
'$cancel' => DI::l10n()->t('Cancel'), '$cancel' => DI::l10n()->t('Cancel'),
]); ]);
@ -964,7 +952,7 @@ class Contact extends BaseModule
if (DBA::isResult($contact)) { if (DBA::isResult($contact)) {
DI::page()['aside'] = ''; DI::page()['aside'] = '';
$profiledata = Model\Contact::getByURL($contact['url'], false); $profiledata = Model\Contact::getByURLForUser($contact['url'], local_user());
Model\Profile::load($a, '', $profiledata, true); Model\Profile::load($a, '', $profiledata, true);
@ -987,7 +975,7 @@ class Contact extends BaseModule
if (DBA::isResult($contact)) { if (DBA::isResult($contact)) {
DI::page()['aside'] = ''; DI::page()['aside'] = '';
$profiledata = Model\Contact::getByURL($contact['url'], false); $profiledata = Model\Contact::getByURLForUser($contact['url'], local_user());
if (local_user() && in_array($profiledata['network'], Protocol::FEDERATED)) { if (local_user() && in_array($profiledata['network'], Protocol::FEDERATED)) {
$profiledata['remoteconnect'] = DI::baseUrl() . '/follow?url=' . urlencode($profiledata['url']); $profiledata['remoteconnect'] = DI::baseUrl() . '/follow?url=' . urlencode($profiledata['url']);

View file

@ -95,7 +95,7 @@ class Friendica extends BaseModule
'about' => DI::l10n()->t('This is Friendica, version %s that is running at the web location %s. The database version is %s, the post update version is %s.', 'about' => DI::l10n()->t('This is Friendica, version %s that is running at the web location %s. The database version is %s, the post update version is %s.',
'<strong>' . FRIENDICA_VERSION . '</strong>', '<strong>' . FRIENDICA_VERSION . '</strong>',
DI::baseUrl()->get(), DI::baseUrl()->get(),
'<strong>' . DB_UPDATE_VERSION . '</strong>', '<strong>' . DB_UPDATE_VERSION . '/' . $config->get('system', 'build') .'</strong>',
'<strong>' . $config->get('system', 'post_update_version') . '</strong>'), '<strong>' . $config->get('system', 'post_update_version') . '</strong>'),
'friendica' => DI::l10n()->t('Please visit <a href="https://friendi.ca">Friendi.ca</a> to learn more about the Friendica project.'), 'friendica' => DI::l10n()->t('Please visit <a href="https://friendi.ca">Friendi.ca</a> to learn more about the Friendica project.'),
'bugs' => DI::l10n()->t('Bug reports and issues: please visit') . ' ' . '<a href="https://github.com/friendica/friendica/issues?state=open">' . DI::l10n()->t('the bugtracker at github') . '</a>', 'bugs' => DI::l10n()->t('Bug reports and issues: please visit') . ' ' . '<a href="https://github.com/friendica/friendica/issues?state=open">' . DI::l10n()->t('the bugtracker at github') . '</a>',

View file

@ -131,7 +131,7 @@ class Group extends BaseModule
throw new \Exception(DI::l10n()->t('Bad request.'), 400); throw new \Exception(DI::l10n()->t('Bad request.'), 400);
} }
notice($message); info($message);
System::jsonExit(['status' => 'OK', 'message' => $message]); System::jsonExit(['status' => 'OK', 'message' => $message]);
} catch (\Exception $e) { } catch (\Exception $e) {
notice($e->getMessage()); notice($e->getMessage());

View file

@ -119,7 +119,7 @@ class Invite extends BaseModule
} }
} }
notice(DI::l10n()->tt('%d message sent.', '%d messages sent.', $total)); info(DI::l10n()->tt('%d message sent.', '%d messages sent.', $total));
} }
public static function content(array $parameters = []) public static function content(array $parameters = [])

View file

@ -232,7 +232,18 @@ class Status extends BaseProfile
$items = DBA::toArray($items_stmt); $items = DBA::toArray($items_stmt);
if ($pager->getStart() == 0 && !empty($a->profile['uid'])) { if ($pager->getStart() == 0 && !empty($a->profile['uid'])) {
$pinned_items = Item::selectPinned($a->profile['uid'], ['uri', 'pinned']); $condition = ['private' => [Item::PUBLIC, Item::UNLISTED]];
if (remote_user()) {
$permissionSets = DI::permissionSet()->selectByContactId(remote_user(), $a->profile['uid']);
if (!empty($permissionSets)) {
$condition = ['psid' => array_merge($permissionSets->column('id'),
[DI::permissionSet()->getIdFromACL($a->profile['uid'], '', '', '', '')])];
}
} elseif ($a->profile['uid'] == local_user()) {
$condition = [];
}
$pinned_items = Item::selectPinned($a->profile['uid'], ['uri', 'pinned'], $condition);
$pinned = Item::inArray($pinned_items); $pinned = Item::inArray($pinned_items);
$items = array_merge($items, $pinned); $items = array_merge($items, $pinned);
} }

View file

@ -34,6 +34,7 @@ use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\ItemContent;
use Friendica\Model\Tag; use Friendica\Model\Tag;
use Friendica\Module\BaseSearch; use Friendica\Module\BaseSearch;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
@ -131,6 +132,14 @@ class Index extends BaseSearch
} }
} }
// Don't perform a fulltext or tag search on search results that look like an URL
// Tags don't look like an URL and the fulltext search does only work with natural words
if (parse_url($search, PHP_URL_SCHEME) && parse_url($search, PHP_URL_HOST)) {
Logger::info('Skipping tag and fulltext search since the search looks like a URL.', ['q' => $search]);
notice(DI::l10n()->t('No results.'));
return $o;
}
$tag = $tag || DI::config()->get('system', 'only_tag_search'); $tag = $tag || DI::config()->get('system', 'only_tag_search');
// Here is the way permissions work in the search module... // Here is the way permissions work in the search module...
@ -151,31 +160,19 @@ class Index extends BaseSearch
if ($tag) { if ($tag) {
Logger::info('Start tag search.', ['q' => $search]); Logger::info('Start tag search.', ['q' => $search]);
$uriids = Tag::getURIIdListByTag($search, local_user(), $pager->getStart(), $pager->getItemsPerPage()); $uriids = Tag::getURIIdListByTag($search, local_user(), $pager->getStart(), $pager->getItemsPerPage());
$count = Tag::countByTag($search, local_user());
if (!empty($uriids)) {
$params = ['order' => ['id' => true], 'group_by' => ['uri-id']];
$items = Item::selectForUser(local_user(), [], ['uri-id' => $uriids], $params);
$r = Item::inArray($items);
} else {
$r = [];
}
} else { } else {
Logger::info('Start fulltext search.', ['q' => $search]); Logger::info('Start fulltext search.', ['q' => $search]);
$uriids = ItemContent::getURIIdListBySearch($search, local_user(), $pager->getStart(), $pager->getItemsPerPage());
$condition = [ $count = ItemContent::countBySearch($search, local_user());
"(`uid` = 0 OR (`uid` = ? AND NOT `global`))
AND `body` LIKE CONCAT('%',?,'%')",
local_user(), $search
];
$params = [
'order' => ['id' => true],
'limit' => [$pager->getStart(), $pager->getItemsPerPage()]
];
$items = Item::selectForUser(local_user(), [], $condition, $params);
$r = Item::inArray($items);
} }
if (!DBA::isResult($r)) { if (!empty($uriids)) {
$params = ['order' => ['id' => true], 'group_by' => ['uri-id']];
$items = Item::inArray(Item::selectForUser(local_user(), [], ['uri-id' => $uriids], $params));
}
if (empty($items)) {
notice(DI::l10n()->t('No results.')); notice(DI::l10n()->t('No results.'));
return $o; return $o;
} }
@ -192,9 +189,9 @@ class Index extends BaseSearch
Logger::info('Start Conversation.', ['q' => $search]); Logger::info('Start Conversation.', ['q' => $search]);
$o .= conversation(DI::app(), $r, 'search', false, false, 'commented', local_user()); $o .= conversation(DI::app(), $items, 'search', false, false, 'commented', local_user());
$o .= $pager->renderMinimal(count($r)); $o .= $pager->renderMinimal($count);
return $o; return $o;
} }

View file

@ -57,7 +57,7 @@ class Recovery extends BaseModule
if (RecoveryCode::existsForUser(local_user(), $recovery_code)) { if (RecoveryCode::existsForUser(local_user(), $recovery_code)) {
RecoveryCode::markUsedForUser(local_user(), $recovery_code); RecoveryCode::markUsedForUser(local_user(), $recovery_code);
Session::set('2fa', true); Session::set('2fa', true);
notice(DI::l10n()->t('Remaining recovery codes: %d', RecoveryCode::countValidForUser(local_user()))); info(DI::l10n()->t('Remaining recovery codes: %d', RecoveryCode::countValidForUser(local_user())));
DI::auth()->setForUser($a, $a->user, true, true); DI::auth()->setForUser($a, $a->user, true, true);
} else { } else {

View file

@ -187,7 +187,7 @@ class Crop extends BaseSettings
Worker::add(PRIORITY_LOW, 'Directory', Session::get('my_url')); Worker::add(PRIORITY_LOW, 'Directory', Session::get('my_url'));
} }
notice(DI::l10n()->t('Profile picture successfully updated.')); info(DI::l10n()->t('Profile picture successfully updated.'));
DI::baseUrl()->redirect('profile/' . DI::app()->user['nickname']); DI::baseUrl()->redirect('profile/' . DI::app()->user['nickname']);
} }

View file

@ -74,13 +74,13 @@ class AppSpecific extends BaseSettings
DI::baseUrl()->redirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password')); DI::baseUrl()->redirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password'));
} else { } else {
self::$appSpecificPassword = AppSpecificPassword::generateForUser(local_user(), $_POST['description'] ?? ''); self::$appSpecificPassword = AppSpecificPassword::generateForUser(local_user(), $_POST['description'] ?? '');
notice(DI::l10n()->t('New app-specific password generated.')); info(DI::l10n()->t('New app-specific password generated.'));
} }
break; break;
case 'revoke_all' : case 'revoke_all' :
AppSpecificPassword::deleteAllForUser(local_user()); AppSpecificPassword::deleteAllForUser(local_user());
notice(DI::l10n()->t('App-specific passwords successfully revoked.')); info(DI::l10n()->t('App-specific passwords successfully revoked.'));
DI::baseUrl()->redirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password')); DI::baseUrl()->redirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password'));
break; break;
} }
@ -90,7 +90,7 @@ class AppSpecific extends BaseSettings
self::checkFormSecurityTokenRedirectOnError('settings/2fa/app_specific', 'settings_2fa_app_specific'); self::checkFormSecurityTokenRedirectOnError('settings/2fa/app_specific', 'settings_2fa_app_specific');
if (AppSpecificPassword::deleteForUser(local_user(), $_POST['revoke_id'])) { if (AppSpecificPassword::deleteForUser(local_user(), $_POST['revoke_id'])) {
notice(DI::l10n()->t('App-specific password successfully revoked.')); info(DI::l10n()->t('App-specific password successfully revoked.'));
} }
DI::baseUrl()->redirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password')); DI::baseUrl()->redirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password'));

View file

@ -64,7 +64,7 @@ class Index extends BaseSettings
DI::pConfig()->delete(local_user(), '2fa', 'verified'); DI::pConfig()->delete(local_user(), '2fa', 'verified');
Session::remove('2fa'); Session::remove('2fa');
notice(DI::l10n()->t('Two-factor authentication successfully disabled.')); info(DI::l10n()->t('Two-factor authentication successfully disabled.'));
DI::baseUrl()->redirect('settings/2fa'); DI::baseUrl()->redirect('settings/2fa');
} }
break; break;

View file

@ -63,7 +63,7 @@ class Recovery extends BaseSettings
if ($_POST['action'] == 'regenerate') { if ($_POST['action'] == 'regenerate') {
RecoveryCode::regenerateForUser(local_user()); RecoveryCode::regenerateForUser(local_user());
notice(DI::l10n()->t('New recovery codes successfully generated.')); info(DI::l10n()->t('New recovery codes successfully generated.'));
DI::baseUrl()->redirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password')); DI::baseUrl()->redirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password'));
} }
} }

View file

@ -75,7 +75,7 @@ class Verify extends BaseSettings
DI::pConfig()->set(local_user(), '2fa', 'verified', true); DI::pConfig()->set(local_user(), '2fa', 'verified', true);
Session::set('2fa', true); Session::set('2fa', true);
notice(DI::l10n()->t('Two-factor authentication successfully activated.')); info(DI::l10n()->t('Two-factor authentication successfully activated.'));
DI::baseUrl()->redirect('settings/2fa'); DI::baseUrl()->redirect('settings/2fa');
} else { } else {

View file

@ -192,7 +192,7 @@ class HTTPRequest implements IHTTPRequest
$curlResponse = new CurlResult($url, $s, $curl_info, curl_errno($ch), curl_error($ch)); $curlResponse = new CurlResult($url, $s, $curl_info, curl_errno($ch), curl_error($ch));
if ($curlResponse->isRedirectUrl()) { if (!Network::isRedirectBlocked($url) && $curlResponse->isRedirectUrl()) {
$redirects++; $redirects++;
$this->logger->notice('Curl redirect.', ['url' => $url, 'to' => $curlResponse->getRedirectUrl()]); $this->logger->notice('Curl redirect.', ['url' => $url, 'to' => $curlResponse->getRedirectUrl()]);
@curl_close($ch); @curl_close($ch);
@ -280,7 +280,7 @@ class HTTPRequest implements IHTTPRequest
$curlResponse = new CurlResult($url, $s, $curl_info, curl_errno($ch), curl_error($ch)); $curlResponse = new CurlResult($url, $s, $curl_info, curl_errno($ch), curl_error($ch));
if ($curlResponse->isRedirectUrl()) { if (!Network::isRedirectBlocked($url) && $curlResponse->isRedirectUrl()) {
$redirects++; $redirects++;
$this->logger->info('Post redirect.', ['url' => $url, 'to' => $curlResponse->getRedirectUrl()]); $this->logger->info('Post redirect.', ['url' => $url, 'to' => $curlResponse->getRedirectUrl()]);
curl_close($ch); curl_close($ch);
@ -321,6 +321,11 @@ class HTTPRequest implements IHTTPRequest
return $url; return $url;
} }
if (Network::isRedirectBlocked($url)) {
$this->logger->info('Domain should not be redirected.', ['url' => $url]);
return $url;
}
$url = Network::stripTrackingQueryParams($url); $url = Network::stripTrackingQueryParams($url);
if ($depth > 10) { if ($depth > 10) {

View file

@ -93,15 +93,17 @@ class Probe
"following", "followers", "inbox", "outbox", "sharedinbox", "following", "followers", "inbox", "outbox", "sharedinbox",
"priority", "network", "pubkey", "manually-approve", "baseurl", "gsid"]; "priority", "network", "pubkey", "manually-approve", "baseurl", "gsid"];
$numeric_fields = ["gsid", "hide", "account-type", "manually-approve"];
$newdata = []; $newdata = [];
foreach ($fields as $field) { foreach ($fields as $field) {
if (isset($data[$field])) { if (isset($data[$field])) {
if (in_array($field, ["gsid", "hide", "account-type", "manually-approve"])) { if (in_array($field, $numeric_fields)) {
$newdata[$field] = (int)$data[$field]; $newdata[$field] = (int)$data[$field];
} else { } else {
$newdata[$field] = $data[$field]; $newdata[$field] = $data[$field];
} }
} elseif ($field != "gsid") { } elseif (!in_array($field, $numeric_fields)) {
$newdata[$field] = ""; $newdata[$field] = "";
} else { } else {
$newdata[$field] = null; $newdata[$field] = null;

View file

@ -0,0 +1,55 @@
<?php
/**
* @copyright Copyright (C) 2020, Friendica
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Object\Api\Mastodon;
use Friendica\BaseEntity;
/**
* Class Activity
*
* @see https://docs.joinmastodon.org/entities/activity
*/
class Activity extends BaseEntity
{
/** @var string (UNIX Timestamp) */
protected $week;
/** @var string */
protected $statuses;
/** @var string */
protected $logins;
/** @var string */
protected $registrations;
/**
* Creates an activity
*
* @param array $item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function __construct(int $week, int $statuses, int $logins, int $registrations)
{
$this->week = (string)$week;
$this->statuses = (string)$statuses;
$this->logins = (string)$logins;
$this->registrations = (string)$registrations;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @copyright Copyright (C) 2020, Friendica
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Object\Api\Mastodon;
use Friendica\BaseEntity;
/**
* Class Application
*
* @see https://docs.joinmastodon.org/entities/application
*/
class Application extends BaseEntity
{
/** @var string */
protected $name;
/**
* Creates an application entry
*
* @param array $item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function __construct(string $name)
{
$this->name = $name;
}
}

View file

@ -23,6 +23,7 @@ namespace Friendica\Object\Api\Mastodon;
use Friendica\BaseEntity; use Friendica\BaseEntity;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Object\Api\Mastodon\Status\Counts;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
/** /**
@ -95,7 +96,7 @@ class Status extends BaseEntity
* @param array $item * @param array $item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public function __construct(array $item, Account $account) public function __construct(array $item, Account $account, Counts $counts)
{ {
$this->id = (string)$item['uri-id']; $this->id = (string)$item['uri-id'];
$this->created_at = DateTimeFormat::utc($item['created'], DateTimeFormat::ATOM); $this->created_at = DateTimeFormat::utc($item['created'], DateTimeFormat::ATOM);
@ -114,9 +115,9 @@ class Status extends BaseEntity
$this->language = null; $this->language = null;
$this->uri = $item['uri']; $this->uri = $item['uri'];
$this->url = $item['plink'] ?? null; $this->url = $item['plink'] ?? null;
$this->replies_count = 0; $this->replies_count = $counts->replies;
$this->reblogs_count = 0; $this->reblogs_count = $counts->reblogs;
$this->favourites_count = 0; $this->favourites_count = $counts->favourites;
$this->favourited = false; $this->favourited = false;
$this->reblogged = false; $this->reblogged = false;
$this->muted = false; $this->muted = false;

View file

@ -0,0 +1,56 @@
<?php
/**
* @copyright Copyright (C) 2020, Friendica
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Object\Api\Mastodon\Status;
/**
* Class Counts
*
* @see https://docs.joinmastodon.org/entities/status
*/
class Counts
{
/** @var int */
protected $replies;
/** @var int */
protected $reblogs;
/** @var int */
protected $favourites;
/**
* Creates a status count object
*
* @param int $replies
* @param int $reblogs
* @param int $favourites
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function __construct(int $replies, int $reblogs, int $favourites)
{
$this->replies = $replies;
$this->reblogs = $reblogs;
$this->favourites = $favourites;
}
public function __get($name) {
return $this->$name;
}
}

View file

@ -89,7 +89,7 @@ class User extends BaseEntity
*/ */
public function __construct(array $publicContact, array $apcontact = [], array $userContact = [], $skip_status = false, $include_user_entities = true) public function __construct(array $publicContact, array $apcontact = [], array $userContact = [], $skip_status = false, $include_user_entities = true)
{ {
$this->id = $publicContact['id']; $this->id = (int)$publicContact['id'];
$this->id_str = (string) $publicContact['id']; $this->id_str = (string) $publicContact['id'];
$this->name = $publicContact['name']; $this->name = $publicContact['name'];
$this->screen_name = $publicContact['nick'] ?: $publicContact['name']; $this->screen_name = $publicContact['nick'] ?: $publicContact['name'];
@ -143,10 +143,10 @@ class User extends BaseEntity
$this->notifications = false; $this->notifications = false;
// Friendica-specific // Friendica-specific
$this->uid = $userContact['uid'] ?? 0; $this->uid = (int)$userContact['uid'] ?? 0;
$this->cid = $userContact['id'] ?? 0; $this->cid = (int)$userContact['id'] ?? 0;
$this->pid = $publicContact['id']; $this->pid = (int)$publicContact['id'];
$this->self = $userContact['self'] ?? false; $this->self = (boolean)$userContact['self'] ?? false;
$this->network = $publicContact['network']; $this->network = $publicContact['network'];
$this->statusnet_profile_url = $publicContact['url']; $this->statusnet_profile_url = $publicContact['url'];
} }

View file

@ -83,11 +83,18 @@ interface IEmail extends JsonSerializable
function getMessage(bool $plain = false); function getMessage(bool $plain = false);
/** /**
* Gets any additional mail header * Gets the additional mail header array
*
* @return string[][]
*/
function getAdditionalMailHeader();
/**
* Gets the additional mail header as string - EOL separated
* *
* @return string * @return string
*/ */
function getAdditionalMailHeader(); function getAdditionalMailHeaderString();
/** /**
* Returns the current email with a new recipient * Returns the current email with a new recipient

View file

@ -47,14 +47,14 @@ class Email implements IEmail
/** @var string */ /** @var string */
private $msgText; private $msgText;
/** @var string */ /** @var string[][] */
private $additionalMailHeader = ''; private $additionalMailHeader;
/** @var int|null */ /** @var int|null */
private $toUid = null; private $toUid;
public function __construct(string $fromName, string $fromAddress, string $replyTo, string $toAddress, public function __construct(string $fromName, string $fromAddress, string $replyTo, string $toAddress,
string $subject, string $msgHtml, string $msgText, string $subject, string $msgHtml, string $msgText,
string $additionalMailHeader = '', int $toUid = null) array $additionalMailHeader = [], int $toUid = null)
{ {
$this->fromName = $fromName; $this->fromName = $fromName;
$this->fromAddress = $fromAddress; $this->fromAddress = $fromAddress;
@ -127,6 +127,25 @@ class Email implements IEmail
return $this->additionalMailHeader; return $this->additionalMailHeader;
} }
/**
* {@inheritDoc}
*/
public function getAdditionalMailHeaderString()
{
$headerString = '';
foreach ($this->additionalMailHeader as $name => $values) {
if (is_array($values)) {
foreach ($values as $value) {
$headerString .= $name . ': ' . $value . '\n';
}
} else {
$headerString .= $name . ': ' . $values . '\n';
}
}
return $headerString;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View file

@ -221,15 +221,14 @@ class Post
$delete = $origin ? DI::l10n()->t('Delete globally') : DI::l10n()->t('Remove locally'); $delete = $origin ? DI::l10n()->t('Delete globally') : DI::l10n()->t('Remove locally');
} }
$drop = [ $drop = false;
'dropping' => $dropping, if (local_user()) {
'pagedrop' => $item['pagedrop'], $drop = [
'select' => DI::l10n()->t('Select'), 'dropping' => $dropping,
'delete' => $delete, 'pagedrop' => $item['pagedrop'],
]; 'select' => DI::l10n()->t('Select'),
'delete' => $delete,
if (!local_user()) { ];
$drop = false;
} }
$filer = (($conv->getProfileOwner() == local_user() && ($item['uid'] != 0)) ? DI::l10n()->t("save to folder") : false); $filer = (($conv->getProfileOwner() == local_user() && ($item['uid'] != 0)) ? DI::l10n()->t("save to folder") : false);
@ -254,7 +253,7 @@ class Post
$locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => '']; $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
Hook::callAll('render_location', $locate); Hook::callAll('render_location', $locate);
$location = ((strlen($locate['html'])) ? $locate['html'] : render_location_dummy($locate)); $location_html = $locate['html'] ?: Strings::escapeHtml($locate['location'] ?: $locate['coord'] ?: '');
// process action responses - e.g. like/dislike/attend/agree/whatever // process action responses - e.g. like/dislike/attend/agree/whatever
$response_verbs = ['like', 'dislike', 'announce']; $response_verbs = ['like', 'dislike', 'announce'];
@ -349,7 +348,7 @@ class Post
} }
} }
$comment = $this->getCommentBox($indent); $comment_html = $this->getCommentBox($indent);
if (strcmp(DateTimeFormat::utc($item['created']), DateTimeFormat::utc('now - 12 hours')) > 0) { if (strcmp(DateTimeFormat::utc($item['created']), DateTimeFormat::utc('now - 12 hours')) > 0) {
$shiny = 'shiny'; $shiny = 'shiny';
@ -357,23 +356,16 @@ class Post
localize_item($item); localize_item($item);
$body = Item::prepareBody($item, true); $body_html = Item::prepareBody($item, true);
list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item); list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item);
$body_e = $body;
$text_e = strip_tags($body);
$name_e = $profile_name;
if (!empty($item['content-warning']) && DI::pConfig()->get(local_user(), 'system', 'disable_cw', false)) { if (!empty($item['content-warning']) && DI::pConfig()->get(local_user(), 'system', 'disable_cw', false)) {
$title_e = ucfirst($item['content-warning']); $title = ucfirst($item['content-warning']);
} else { } else {
$title_e = $item['title']; $title = $item['title'];
} }
$location_e = $location;
$owner_name_e = $this->getOwnerName();
if (DI::pConfig()->get(local_user(), 'system', 'hide_dislike')) { if (DI::pConfig()->get(local_user(), 'system', 'hide_dislike')) {
$buttons['dislike'] = false; $buttons['dislike'] = false;
} }
@ -414,8 +406,8 @@ class Post
} elseif (DI::config()->get('debug', 'show_direction')) { } elseif (DI::config()->get('debug', 'show_direction')) {
$conversation = DBA::selectFirst('conversation', ['direction'], ['item-uri' => $item['uri']]); $conversation = DBA::selectFirst('conversation', ['direction'], ['item-uri' => $item['uri']]);
if (!empty($conversation['direction']) && in_array($conversation['direction'], [1, 2])) { if (!empty($conversation['direction']) && in_array($conversation['direction'], [1, 2])) {
$title = [1 => DI::l10n()->t('Pushed'), 2 => DI::l10n()->t('Pulled')]; $direction_title = [1 => DI::l10n()->t('Pushed'), 2 => DI::l10n()->t('Pulled')];
$direction = ['direction' => $conversation['direction'], 'title' => $title[$conversation['direction']]]; $direction = ['direction' => $conversation['direction'], 'title' => $direction_title[$conversation['direction']]];
} }
} }
@ -433,8 +425,8 @@ class Post
'has_folders' => ((count($folders)) ? 'true' : ''), 'has_folders' => ((count($folders)) ? 'true' : ''),
'categories' => $categories, 'categories' => $categories,
'folders' => $folders, 'folders' => $folders,
'body' => $body_e, 'body_html' => $body_html,
'text' => $text_e, 'text' => strip_tags($body_html),
'id' => $this->getId(), 'id' => $this->getId(),
'guid' => urlencode($item['guid']), 'guid' => urlencode($item['guid']),
'isevent' => $isevent, 'isevent' => $isevent,
@ -446,24 +438,24 @@ class Post
'wall' => DI::l10n()->t('Wall-to-Wall'), 'wall' => DI::l10n()->t('Wall-to-Wall'),
'vwall' => DI::l10n()->t('via Wall-To-Wall:'), 'vwall' => DI::l10n()->t('via Wall-To-Wall:'),
'profile_url' => $profile_link, 'profile_url' => $profile_link,
'item_photo_menu' => item_photo_menu($item), 'name' => $profile_name,
'name' => $name_e, 'item_photo_menu_html' => item_photo_menu($item),
'thumb' => DI::baseUrl()->remove($item['author-avatar']), 'thumb' => DI::baseUrl()->remove($item['author-avatar']),
'osparkle' => $osparkle, 'osparkle' => $osparkle,
'sparkle' => $sparkle, 'sparkle' => $sparkle,
'title' => $title_e, 'title' => $title,
'localtime' => DateTimeFormat::local($item['created'], 'r'), 'localtime' => DateTimeFormat::local($item['created'], 'r'),
'ago' => $item['app'] ? DI::l10n()->t('%s from %s', $ago, $item['app']) : $ago, 'ago' => $item['app'] ? DI::l10n()->t('%s from %s', $ago, $item['app']) : $ago,
'app' => $item['app'], 'app' => $item['app'],
'created' => $ago, 'created' => $ago,
'lock' => $lock, 'lock' => $lock,
'location' => $location_e, 'location_html' => $location_html,
'indent' => $indent, 'indent' => $indent,
'shiny' => $shiny, 'shiny' => $shiny,
'owner_self' => $item['author-link'] == Session::get('my_url'), 'owner_self' => $item['author-link'] == Session::get('my_url'),
'owner_url' => $this->getOwnerUrl(), 'owner_url' => $this->getOwnerUrl(),
'owner_photo' => DI::baseUrl()->remove($item['owner-avatar']), 'owner_photo' => DI::baseUrl()->remove($item['owner-avatar']),
'owner_name' => $owner_name_e, 'owner_name' => $this->getOwnerName(),
'plink' => Item::getPlink($item), 'plink' => Item::getPlink($item),
'edpost' => $edpost, 'edpost' => $edpost,
'ispinned' => $ispinned, 'ispinned' => $ispinned,
@ -476,12 +468,12 @@ class Post
'filer' => $filer, 'filer' => $filer,
'drop' => $drop, 'drop' => $drop,
'vote' => $buttons, 'vote' => $buttons,
'like' => $responses['like']['output'], 'like_html' => $responses['like']['output'],
'dislike' => $responses['dislike']['output'], 'dislike_html' => $responses['dislike']['output'],
'responses' => $responses, 'responses' => $responses,
'switchcomment' => DI::l10n()->t('Comment'), 'switchcomment' => DI::l10n()->t('Comment'),
'reply_label' => DI::l10n()->t('Reply to %s', $name_e), 'reply_label' => DI::l10n()->t('Reply to %s', $profile_name),
'comment' => $comment, 'comment_html' => $comment_html,
'remote_comment' => $remote_comment, 'remote_comment' => $remote_comment,
'menu' => DI::l10n()->t('More'), 'menu' => DI::l10n()->t('More'),
'previewing' => $conv->isPreview() ? ' preview ' : '', 'previewing' => $conv->isPreview() ? ' preview ' : '',
@ -497,6 +489,7 @@ class Post
'uriid' => $item['uri-id'], 'uriid' => $item['uri-id'],
'return' => (DI::args()->getCommand()) ? bin2hex(DI::args()->getCommand()) : '', 'return' => (DI::args()->getCommand()) ? bin2hex(DI::args()->getCommand()) : '',
'direction' => $direction, 'direction' => $direction,
'reshared' => $item['reshared'] ?? '',
'delivery' => [ 'delivery' => [
'queue_count' => $item['delivery_queue_count'], 'queue_count' => $item['delivery_queue_count'],
'queue_done' => $item['delivery_queue_done'] + $item['delivery_queue_failed'], /// @todo Possibly display it separately in the future 'queue_done' => $item['delivery_queue_done'] + $item['delivery_queue_failed'], /// @todo Possibly display it separately in the future
@ -904,21 +897,16 @@ class Post
$comment_box = ''; $comment_box = '';
$conv = $this->getThread(); $conv = $this->getThread();
$ww = '';
if (($conv->getMode() === 'network') && $this->isWallToWall()) {
$ww = 'ww';
}
if ($conv->isWritable() && $this->isWritable()) { if ($conv->isWritable() && $this->isWritable()) {
$qcomment = null;
/* /*
* Hmmm, code depending on the presence of a particular addon? * Hmmm, code depending on the presence of a particular addon?
* This should be better if done by a hook * This should be better if done by a hook
*/ */
$qcomment = null;
if (Addon::isEnabled('qcomment')) { if (Addon::isEnabled('qcomment')) {
$qc = ((local_user()) ? DI::pConfig()->get(local_user(), 'qcomment', 'words') : null); $words = DI::pConfig()->get(local_user(), 'qcomment', 'words');
$qcomment = (($qc) ? explode("\n", $qc) : null); $qcomment = $words ? explode("\n", $words) : [];
} }
// Fetch the user id from the parent when the owner user is empty // Fetch the user id from the parent when the owner user is empty
@ -960,7 +948,6 @@ class Post
'$preview' => DI::l10n()->t('Preview'), '$preview' => DI::l10n()->t('Preview'),
'$indent' => $indent, '$indent' => $indent,
'$sourceapp' => DI::l10n()->t($a->sourcename), '$sourceapp' => DI::l10n()->t($a->sourcename),
'$ww' => $conv->getMode() === 'network' ? $ww : '',
'$rand_num' => Crypto::randomDigits(12) '$rand_num' => Crypto::randomDigits(12)
]); ]);
} }
@ -990,7 +977,7 @@ class Post
if ($this->isToplevel()) { if ($this->isToplevel()) {
if ($conv->getMode() !== 'profile') { if ($conv->getMode() !== 'profile') {
if ($this->getDataValue('wall') && !$this->getDataValue('self')) { if ($this->getDataValue('wall') && !$this->getDataValue('self') && !empty($a->page_contact)) {
// On the network page, I am the owner. On the display page it will be the profile owner. // On the network page, I am the owner. On the display page it will be the profile owner.
// This will have been stored in $a->page_contact by our calling page. // This will have been stored in $a->page_contact by our calling page.
// Put this person as the wall owner of the wall-to-wall notice. // Put this person as the wall owner of the wall-to-wall notice.

View file

@ -206,9 +206,6 @@ class Processor
} else { } else {
$item['gravity'] = GRAVITY_COMMENT; $item['gravity'] = GRAVITY_COMMENT;
$item['object-type'] = Activity\ObjectType::COMMENT; $item['object-type'] = Activity\ObjectType::COMMENT;
// Ensure that the comment reaches all receivers of the referring post
$activity['receiver'] = self::addReceivers($activity);
} }
if (empty($activity['directmessage']) && ($activity['id'] != $activity['reply-to-id']) && !Item::exists(['uri' => $activity['reply-to-id']])) { if (empty($activity['directmessage']) && ($activity['id'] != $activity['reply-to-id']) && !Item::exists(['uri' => $activity['reply-to-id']])) {
@ -275,6 +272,7 @@ class Processor
$item = self::processContent($activity, $item); $item = self::processContent($activity, $item);
if (empty($item)) { if (empty($item)) {
Logger::info('Message was not processed');
return []; return [];
} }
@ -330,35 +328,6 @@ class Processor
} }
} }
/**
* Add users to the receiver list of the given public activity.
* This is used to ensure that the activity will be stored in every thread.
*
* @param array $activity Activity array
* @return array Modified receiver list
*/
private static function addReceivers(array $activity)
{
if (!in_array(0, $activity['receiver'])) {
// Private activities will not be modified
return $activity['receiver'];
}
// Add all owners of the referring item to the receivers
$original = $receivers = $activity['receiver'];
$items = Item::select(['uid'], ['uri' => $activity['object_id']]);
while ($item = DBA::fetch($items)) {
$receivers['uid:' . $item['uid']] = $item['uid'];
}
DBA::close($items);
if (count($original) != count($receivers)) {
Logger::info('Improved data', ['id' => $activity['id'], 'object' => $activity['object_id'], 'original' => $original, 'improved' => $receivers]);
}
return $receivers;
}
/** /**
* Prepare the item array for an activity * Prepare the item array for an activity
* *
@ -377,8 +346,6 @@ class Processor
$item['diaspora_signed_text'] = $activity['diaspora:like'] ?? ''; $item['diaspora_signed_text'] = $activity['diaspora:like'] ?? '';
$activity['receiver'] = self::addReceivers($activity);
self::postItem($activity, $item); self::postItem($activity, $item);
} }
@ -527,6 +494,33 @@ class Processor
$item['uid'] = $receiver; $item['uid'] = $receiver;
$type = $activity['reception_type'][$receiver] ?? Receiver::TARGET_UNKNOWN;
switch($type) {
case Receiver::TARGET_TO:
$item['post-type'] = Item::PT_TO;
break;
case Receiver::TARGET_CC:
$item['post-type'] = Item::PT_CC;
break;
case Receiver::TARGET_BTO:
$item['post-type'] = Item::PT_BTO;
break;
case Receiver::TARGET_BCC:
$item['post-type'] = Item::PT_BCC;
break;
case Receiver::TARGET_FOLLOWER:
$item['post-type'] = Item::PT_FOLLOWER;
break;
case Receiver::TARGET_ANSWER:
$item['post-type'] = Item::PT_COMMENT;
break;
case Receiver::TARGET_GLOBAL:
$item['post-type'] = Item::PT_GLOBAL;
break;
default:
$item['post-type'] = Item::PT_ARTICLE;
}
if ($item['isForum'] ?? false) { if ($item['isForum'] ?? false) {
$item['contact-id'] = Contact::getIdForURL($activity['actor'], $receiver); $item['contact-id'] = Contact::getIdForURL($activity['actor'], $receiver);
} else { } else {
@ -718,15 +712,22 @@ class Processor
return ''; return '';
} }
if (!empty($child['author'])) { if (!empty($object['actor'])) {
$actor = $child['author']; $object_actor = $object['actor'];
} elseif (!empty($object['actor'])) {
$actor = $object['actor'];
} elseif (!empty($object['attributedTo'])) { } elseif (!empty($object['attributedTo'])) {
$actor = $object['attributedTo']; $object_actor = $object['attributedTo'];
} else { } else {
// Shouldn't happen // Shouldn't happen
$actor = ''; $object_actor = '';
}
$signer = [$object_actor];
if (!empty($child['author'])) {
$actor = $child['author'];
$signer[] = $actor;
} else {
$actor = $object_actor;
} }
if (!empty($object['published'])) { if (!empty($object['published'])) {
@ -752,7 +753,7 @@ class Processor
$ldactivity['thread-completion'] = true; $ldactivity['thread-completion'] = true;
ActivityPub\Receiver::processActivity($ldactivity, json_encode($activity)); ActivityPub\Receiver::processActivity($ldactivity, json_encode($activity), $uid, true, false, $signer);
Logger::notice('Activity had been fetched and processed.', ['url' => $url, 'object' => $activity['id']]); Logger::notice('Activity had been fetched and processed.', ['url' => $url, 'object' => $activity['id']]);

View file

@ -33,7 +33,6 @@ use Friendica\Model\Item;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Protocol\Activity; use Friendica\Protocol\Activity;
use Friendica\Protocol\ActivityPub; use Friendica\Protocol\ActivityPub;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\HTTPSignature; use Friendica\Util\HTTPSignature;
use Friendica\Util\JsonLD; use Friendica\Util\JsonLD;
use Friendica\Util\LDSignature; use Friendica\Util\LDSignature;
@ -59,6 +58,15 @@ class Receiver
const CONTENT_TYPES = ['as:Note', 'as:Article', 'as:Video', 'as:Image', 'as:Event', 'as:Audio']; const CONTENT_TYPES = ['as:Note', 'as:Article', 'as:Video', 'as:Image', 'as:Event', 'as:Audio'];
const ACTIVITY_TYPES = ['as:Like', 'as:Dislike', 'as:Accept', 'as:Reject', 'as:TentativeAccept']; const ACTIVITY_TYPES = ['as:Like', 'as:Dislike', 'as:Accept', 'as:Reject', 'as:TentativeAccept'];
const TARGET_UNKNOWN = 0;
const TARGET_TO = 1;
const TARGET_CC = 2;
const TARGET_BTO = 3;
const TARGET_BCC = 4;
const TARGET_FOLLOWER = 5;
const TARGET_ANSWER = 6;
const TARGET_GLOBAL = 7;
/** /**
* Checks if the web request is done for the AP protocol * Checks if the web request is done for the AP protocol
* *
@ -80,16 +88,7 @@ class Receiver
*/ */
public static function processInbox($body, $header, $uid) public static function processInbox($body, $header, $uid)
{ {
$http_signer = HTTPSignature::getSigner($body, $header);
if (empty($http_signer)) {
Logger::warning('Invalid HTTP signature, message will be discarded.');
return;
} else {
Logger::info('Valid HTTP signature', ['signer' => $http_signer]);
}
$activity = json_decode($body, true); $activity = json_decode($body, true);
if (empty($activity)) { if (empty($activity)) {
Logger::warning('Invalid body.'); Logger::warning('Invalid body.');
return; return;
@ -99,12 +98,30 @@ class Receiver
$actor = JsonLD::fetchElement($ldactivity, 'as:actor', '@id'); $actor = JsonLD::fetchElement($ldactivity, 'as:actor', '@id');
$apcontact = APContact::getByURL($actor);
if (!empty($apcontact) && ($apcontact['type'] == 'Application') && ($apcontact['nick'] == 'relay')) {
self::processRelayPost($ldactivity);
return;
}
$http_signer = HTTPSignature::getSigner($body, $header);
if (empty($http_signer)) {
Logger::warning('Invalid HTTP signature, message will be discarded.');
return;
} else {
Logger::info('Valid HTTP signature', ['signer' => $http_signer]);
}
$signer = [$http_signer];
Logger::info('Message for user ' . $uid . ' is from actor ' . $actor); Logger::info('Message for user ' . $uid . ' is from actor ' . $actor);
if (LDSignature::isSigned($activity)) { if (LDSignature::isSigned($activity)) {
$ld_signer = LDSignature::getSigner($activity); $ld_signer = LDSignature::getSigner($activity);
if (empty($ld_signer)) { if (empty($ld_signer)) {
Logger::log('Invalid JSON-LD signature from ' . $actor, Logger::DEBUG); Logger::log('Invalid JSON-LD signature from ' . $actor, Logger::DEBUG);
} elseif ($ld_signer != $http_signer) {
$signer[] = $ld_signer;
} }
if (!empty($ld_signer && ($actor == $http_signer))) { if (!empty($ld_signer && ($actor == $http_signer))) {
Logger::log('The HTTP and the JSON-LD signature belong to ' . $ld_signer, Logger::DEBUG); Logger::log('The HTTP and the JSON-LD signature belong to ' . $ld_signer, Logger::DEBUG);
@ -127,7 +144,50 @@ class Receiver
$trust_source = false; $trust_source = false;
} }
self::processActivity($ldactivity, $body, $uid, $trust_source, true); self::processActivity($ldactivity, $body, $uid, $trust_source, true, $signer);
}
/**
* Process incoming posts from relays
*
* @param array $activity
* @return void
*/
private static function processRelayPost(array $activity)
{
$type = JsonLD::fetchElement($activity, '@type');
if (!$type) {
Logger::info('Empty type', ['activity' => $activity]);
return;
}
if ($type != 'as:Announce') {
Logger::info('Not an announcement', ['activity' => $activity]);
return;
}
$object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
if (empty($object_id)) {
Logger::info('No object id found', ['activity' => $activity]);
return;
}
Logger::info('Got relayed message id', ['id' => $object_id]);
$item_id = Item::searchByLink($object_id);
if ($item_id) {
Logger::info('Relayed message already exists', ['id' => $object_id, 'item' => $item_id]);
return;
}
Processor::fetchMissingActivity($object_id);
$item_id = Item::searchByLink($object_id);
if ($item_id) {
Logger::info('Relayed message had been fetched and stored', ['id' => $object_id, 'item' => $item_id]);
} else {
Logger::notice('Relayed message had not been stored', ['id' => $object_id]);
}
} }
/** /**
@ -186,29 +246,53 @@ class Receiver
*/ */
public static function prepareObjectData($activity, $uid, $push, &$trust_source) public static function prepareObjectData($activity, $uid, $push, &$trust_source)
{ {
$id = JsonLD::fetchElement($activity, '@id');
if (!empty($id) && !$trust_source) {
$fetched_activity = ActivityPub::fetchContent($id, $uid ?? 0);
if (!empty($fetched_activity)) {
$object = JsonLD::compact($fetched_activity);
$fetched_id = JsonLD::fetchElement($object, '@id');
if ($fetched_id == $id) {
Logger::info('Activity had been fetched successfully', ['id' => $id]);
$trust_source = true;
$activity = $object;
} else {
Logger::info('Activity id is not equal', ['id' => $id, 'fetched' => $fetched_id]);
}
} else {
Logger::info('Activity could not been fetched', ['id' => $id]);
}
}
$actor = JsonLD::fetchElement($activity, 'as:actor', '@id'); $actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
if (empty($actor)) { if (empty($actor)) {
Logger::log('Empty actor', Logger::DEBUG); Logger::info('Empty actor', ['activity' => $activity]);
return []; return [];
} }
$type = JsonLD::fetchElement($activity, '@type'); $type = JsonLD::fetchElement($activity, '@type');
// Fetch all receivers from to, cc, bto and bcc // Fetch all receivers from to, cc, bto and bcc
$receivers = self::getReceivers($activity, $actor); $receiverdata = self::getReceivers($activity, $actor);
$receivers = $reception_types = [];
foreach ($receiverdata as $key => $data) {
$receivers[$key] = $data['uid'];
$reception_types[$data['uid']] = $data['type'] ?? 0;
}
// When it is a delivery to a personal inbox we add that user to the receivers // When it is a delivery to a personal inbox we add that user to the receivers
if (!empty($uid)) { if (!empty($uid)) {
$additional = ['uid:' . $uid => $uid]; $additional = ['uid:' . $uid => $uid];
$receivers = array_merge($receivers, $additional); $receivers = array_merge($receivers, $additional);
if (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;
}
} else { } else {
// We possibly need some user to fetch private content, // We possibly need some user to fetch private content,
// so we fetch the first out ot the list. // so we fetch the first out ot the list.
$uid = self::getFirstUserFromReceivers($receivers); $uid = self::getFirstUserFromReceivers($receivers);
} }
Logger::log('Receivers: ' . $uid . ' - ' . json_encode($receivers), Logger::DEBUG);
$object_id = JsonLD::fetchElement($activity, 'as:object', '@id'); $object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
if (empty($object_id)) { if (empty($object_id)) {
Logger::log('No object found', Logger::DEBUG); Logger::log('No object found', Logger::DEBUG);
@ -224,11 +308,8 @@ class Receiver
// Fetch the content only on activities where this matters // Fetch the content only on activities where this matters
if (in_array($type, ['as:Create', 'as:Update', 'as:Announce'])) { if (in_array($type, ['as:Create', 'as:Update', 'as:Announce'])) {
if ($type == 'as:Announce') { // Always fetch on "Announce"
$trust_source = false; $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, $uid);
if (empty($object_data)) { if (empty($object_data)) {
Logger::log("Object data couldn't be processed", Logger::DEBUG); Logger::log("Object data couldn't be processed", Logger::DEBUG);
return []; return [];
@ -248,9 +329,6 @@ class Receiver
} else { } else {
$object_data['directmessage'] = JsonLD::fetchElement($activity, 'litepub:directMessage'); $object_data['directmessage'] = JsonLD::fetchElement($activity, 'litepub:directMessage');
} }
// We had been able to retrieve the object data - so we can trust the source
$trust_source = true;
} elseif (in_array($type, array_merge(self::ACTIVITY_TYPES, ['as:Follow'])) && in_array($object_type, self::CONTENT_TYPES)) { } elseif (in_array($type, array_merge(self::ACTIVITY_TYPES, ['as:Follow'])) && in_array($object_type, self::CONTENT_TYPES)) {
// Create a mostly empty array out of the activity data (instead of the object). // Create a mostly empty array out of the activity data (instead of the object).
// This way we later don't have to check for the existence of ech individual array element. // This way we later don't have to check for the existence of ech individual array element.
@ -290,6 +368,19 @@ class Receiver
$object_data['actor'] = $actor; $object_data['actor'] = $actor;
$object_data['item_receiver'] = $receivers; $object_data['item_receiver'] = $receivers;
$object_data['receiver'] = array_merge($object_data['receiver'] ?? [], $receivers); $object_data['receiver'] = array_merge($object_data['receiver'] ?? [], $receivers);
$object_data['reception_type'] = array_merge($object_data['reception_type'] ?? [], $reception_types);
$author = $object_data['author'] ?? $actor;
if (!empty($author) && !empty($object_data['id'])) {
$author_host = parse_url($author, PHP_URL_HOST);
$id_host = parse_url($object_data['id'], PHP_URL_HOST);
if ($author_host == $id_host) {
Logger::info('Valid hosts', ['type' => $type, 'host' => $id_host]);
} else {
Logger::notice('Differing hosts on author and id', ['type' => $type, 'author' => $author_host, 'id' => $id_host]);
$trust_source = false;
}
}
Logger::log('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id'], Logger::DEBUG); Logger::log('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id'], Logger::DEBUG);
@ -322,43 +413,49 @@ class Receiver
* @param boolean $push Message had been pushed to our system * @param boolean $push Message had been pushed to our system
* @throws \Exception * @throws \Exception
*/ */
public static function processActivity($activity, $body = '', $uid = null, $trust_source = false, $push = false) public static function processActivity($activity, string $body = '', int $uid = null, bool $trust_source = false, bool $push = false, array $signer = [])
{ {
$type = JsonLD::fetchElement($activity, '@type'); $type = JsonLD::fetchElement($activity, '@type');
if (!$type) { if (!$type) {
Logger::log('Empty type', Logger::DEBUG); Logger::info('Empty type', ['activity' => $activity]);
return; return;
} }
if (!JsonLD::fetchElement($activity, 'as:object', '@id')) { if (!JsonLD::fetchElement($activity, 'as:object', '@id')) {
Logger::log('Empty object', Logger::DEBUG); Logger::info('Empty object', ['activity' => $activity]);
return; return;
} }
if (!JsonLD::fetchElement($activity, 'as:actor', '@id')) { $actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
Logger::log('Empty actor', Logger::DEBUG); if (empty($actor)) {
Logger::info('Empty actor', ['activity' => $activity]);
return; return;
} }
// Don't trust the source if "actor" differs from "attributedTo". The content could be forged. if (is_array($activity['as:object'])) {
if ($trust_source && ($type == 'as:Create') && is_array($activity['as:object'])) {
$actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
$attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo', '@id'); $attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo', '@id');
$trust_source = ($actor == $attributed_to); } else {
if (!$trust_source) { $attributed_to = '';
Logger::log('Not trusting actor: ' . $actor . '. It differs from attributedTo: ' . $attributed_to, Logger::DEBUG); }
// Test the provided signatures against the actor and "attributedTo"
if ($trust_source) {
if (!empty($attributed_to) && !empty($actor)) {
$trust_source = (in_array($actor, $signer) && in_array($attributed_to, $signer));
} else {
$trust_source = in_array($actor, $signer);
} }
} }
// $trust_source is called by reference and is set to true if the content was retrieved successfully // $trust_source is called by reference and is set to true if the content was retrieved successfully
$object_data = self::prepareObjectData($activity, $uid, $push, $trust_source); $object_data = self::prepareObjectData($activity, $uid, $push, $trust_source);
if (empty($object_data)) { if (empty($object_data)) {
Logger::log('No object data found', Logger::DEBUG); Logger::info('No object data found', ['activity' => $activity]);
return; return;
} }
if (!$trust_source) { if (!$trust_source) {
Logger::log('No trust for activity type "' . $type . '", so we quit now.', Logger::DEBUG); Logger::info('Activity trust could not be achieved.', ['id' => $object_data['object_id'], 'type' => $type, 'signer' => $signer, 'actor' => $actor, 'attributedTo' => $attributed_to]);
return; return;
} }
@ -390,6 +487,11 @@ class Receiver
$object_data['thread-completion'] = true; $object_data['thread-completion'] = true;
$item = ActivityPub\Processor::createItem($object_data); $item = ActivityPub\Processor::createItem($object_data);
if (empty($item)) {
return;
}
$item['post-type'] = Item::PT_ANNOUNCEMENT;
ActivityPub\Processor::postItem($object_data, $item); ActivityPub\Processor::postItem($object_data, $item);
$announce_object_data = self::processObject($activity); $announce_object_data = self::processObject($activity);
@ -498,18 +600,30 @@ class Receiver
*/ */
private static function getReceivers($activity, $actor, $tags = [], $fetch_unlisted = false) private static function getReceivers($activity, $actor, $tags = [], $fetch_unlisted = false)
{ {
$receivers = []; $reply = $receivers = [];
// When it is an answer, we inherite the receivers from the parent // When it is an answer, we inherite the receivers from the parent
$replyto = JsonLD::fetchElement($activity, 'as:inReplyTo', '@id'); $replyto = JsonLD::fetchElement($activity, 'as:inReplyTo', '@id');
if (!empty($replyto)) { if (!empty($replyto)) {
$reply = [$replyto];
// Fix possibly wrong item URI (could be an answer to a plink uri) // Fix possibly wrong item URI (could be an answer to a plink uri)
$fixedReplyTo = Item::getURIByLink($replyto); $fixedReplyTo = Item::getURIByLink($replyto);
$replyto = $fixedReplyTo ?: $replyto; if (!empty($fixedReplyTo)) {
$reply[] = $fixedReplyTo;
}
}
$parents = Item::select(['uid'], ['uri' => $replyto]); // Fetch all posts that refer to the object id
$object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
if (!empty($object_id)) {
$reply[] = $object_id;
}
if (!empty($reply)) {
$parents = Item::select(['uid'], ['uri' => $reply]);
while ($parent = Item::fetch($parents)) { while ($parent = Item::fetch($parents)) {
$receivers['uid:' . $parent['uid']] = $parent['uid']; $receivers['uid:' . $parent['uid']] = ['uid' => $parent['uid'], 'type' => self::TARGET_ANSWER];
} }
} }
@ -519,7 +633,7 @@ class Receiver
Logger::log('Actor: ' . $actor . ' - Followers: ' . $followers, Logger::DEBUG); Logger::log('Actor: ' . $actor . ' - Followers: ' . $followers, Logger::DEBUG);
} else { } else {
Logger::log('Empty actor', Logger::DEBUG); Logger::info('Empty actor', ['activity' => $activity]);
$followers = ''; $followers = '';
} }
@ -531,29 +645,17 @@ class Receiver
foreach ($receiver_list as $receiver) { foreach ($receiver_list as $receiver) {
if ($receiver == self::PUBLIC_COLLECTION) { if ($receiver == self::PUBLIC_COLLECTION) {
$receivers['uid:0'] = 0; $receivers['uid:0'] = ['uid' => 0, 'type' => self::TARGET_GLOBAL];
} }
// Add receiver "-1" for unlisted posts // Add receiver "-1" for unlisted posts
if ($fetch_unlisted && ($receiver == self::PUBLIC_COLLECTION) && ($element == 'as:cc')) { if ($fetch_unlisted && ($receiver == self::PUBLIC_COLLECTION) && ($element == 'as:cc')) {
$receivers['uid:-1'] = -1; $receivers['uid:-1'] = ['uid' => -1, 'type' => self::TARGET_GLOBAL];
}
if (($receiver == self::PUBLIC_COLLECTION) && !empty($actor)) {
// This will most likely catch all OStatus connections to Mastodon
$condition = ['alias' => [$actor, Strings::normaliseLink($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND]
, 'archive' => false, 'pending' => false];
$contacts = DBA::select('contact', ['uid'], $condition);
while ($contact = DBA::fetch($contacts)) {
if ($contact['uid'] != 0) {
$receivers['uid:' . $contact['uid']] = $contact['uid'];
}
}
DBA::close($contacts);
} }
// Fetch the receivers for the public and the followers collection
if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) { if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) {
$receivers = array_merge($receivers, self::getReceiverForActor($actor, $tags)); $receivers = self::getReceiverForActor($actor, $tags, $receivers);
continue; continue;
} }
@ -581,7 +683,25 @@ class Receiver
} }
} }
$receivers['uid:' . $contact['uid']] = $contact['uid']; $type = $receivers['uid:' . $contact['uid']]['type'] ?? self::TARGET_UNKNOWN;
if (in_array($type, [self::TARGET_UNKNOWN, self::TARGET_FOLLOWER, self::TARGET_ANSWER, self::TARGET_GLOBAL])) {
switch ($element) {
case 'as:to':
$type = self::TARGET_TO;
break;
case 'as:cc':
$type = self::TARGET_CC;
break;
case 'as:bto':
$type = self::TARGET_BTO;
break;
case 'as:bcc':
$type = self::TARGET_BCC;
break;
}
$receivers['uid:' . $contact['uid']] = ['uid' => $contact['uid'], 'type' => $type];
}
} }
} }
@ -599,16 +719,26 @@ class Receiver
* @return array with receivers (user id) * @return array with receivers (user id)
* @throws \Exception * @throws \Exception
*/ */
public static function getReceiverForActor($actor, $tags) private static function getReceiverForActor($actor, $tags, $receivers)
{ {
$receivers = []; $basecondition = ['rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER],
$networks = Protocol::FEDERATED; 'network' => Protocol::FEDERATED, 'archive' => false, 'pending' => false];
$condition = ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER],
'network' => $networks, 'archive' => false, 'pending' => false]; $condition = DBA::mergeConditions($basecondition, ['nurl' => Strings::normaliseLink($actor)]);
$contacts = DBA::select('contact', ['uid', 'rel'], $condition); $contacts = DBA::select('contact', ['uid', 'rel'], $condition);
while ($contact = DBA::fetch($contacts)) { while ($contact = DBA::fetch($contacts)) {
if (self::isValidReceiverForActor($contact, $actor, $tags)) { if (empty($receivers['uid:' . $contact['uid']]) && self::isValidReceiverForActor($contact, $actor, $tags)) {
$receivers['uid:' . $contact['uid']] = $contact['uid']; $receivers['uid:' . $contact['uid']] = ['uid' => $contact['uid'], 'type' => self::TARGET_FOLLOWER];
}
}
DBA::close($contacts);
// The queries are split because of performance issues
$condition = DBA::mergeConditions($basecondition, ["`alias` IN (?, ?)", Strings::normaliseLink($actor), $actor]);
$contacts = DBA::select('contact', ['uid', 'rel'], $condition);
while ($contact = DBA::fetch($contacts)) {
if (empty($receivers['uid:' . $contact['uid']]) && self::isValidReceiverForActor($contact, $actor, $tags)) {
$receivers['uid:' . $contact['uid']] = ['uid' => $contact['uid'], 'type' => self::TARGET_FOLLOWER];
} }
} }
DBA::close($contacts); DBA::close($contacts);
@ -699,14 +829,14 @@ class Receiver
} }
foreach ($receivers as $receiver) { foreach ($receivers as $receiver) {
$contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'nurl' => Strings::normaliseLink($actor)]); $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver['uid'], 'network' => Protocol::OSTATUS, 'nurl' => Strings::normaliseLink($actor)]);
if (DBA::isResult($contact)) { if (DBA::isResult($contact)) {
self::switchContact($contact['id'], $receiver, $actor); self::switchContact($contact['id'], $receiver['uid'], $actor);
} }
$contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'alias' => [Strings::normaliseLink($actor), $actor]]); $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver['uid'], 'network' => Protocol::OSTATUS, 'alias' => [Strings::normaliseLink($actor), $actor]]);
if (DBA::isResult($contact)) { if (DBA::isResult($contact)) {
self::switchContact($contact['id'], $receiver, $actor); self::switchContact($contact['id'], $receiver['uid'], $actor);
} }
} }
} }
@ -778,18 +908,29 @@ class Receiver
$data = ActivityPub\Transmitter::createNote($item); $data = ActivityPub\Transmitter::createNote($item);
$object = JsonLD::compact($data); $object = JsonLD::compact($data);
} }
$id = JsonLD::fetchElement($object, '@id');
if (empty($id)) {
Logger::info('Empty id');
return false;
}
if ($id != $object_id) {
Logger::info('Fetched id differs from provided id', ['provided' => $object_id, 'fetched' => $id]);
return false;
}
} else { } else {
Logger::log('Using original object for url ' . $object_id, Logger::DEBUG); Logger::log('Using original object for url ' . $object_id, Logger::DEBUG);
} }
$type = JsonLD::fetchElement($object, '@type'); $type = JsonLD::fetchElement($object, '@type');
if (empty($type)) { if (empty($type)) {
Logger::log('Empty type', Logger::DEBUG); Logger::info('Empty type');
return false; return false;
} }
if (in_array($type, self::CONTENT_TYPES)) { // We currently don't handle 'pt:CacheFile', but with this step we avoid logging
if (in_array($type, self::CONTENT_TYPES) || ($type == 'pt:CacheFile')) {
$object_data = self::processObject($object); $object_data = self::processObject($object);
if (!empty($data)) { if (!empty($data)) {
@ -1189,7 +1330,16 @@ class Receiver
$object_data = self::processAttachmentUrls($object, $object_data); $object_data = self::processAttachmentUrls($object, $object_data);
} }
$object_data['receiver'] = self::getReceivers($object, $object_data['actor'], $object_data['tags'], true); $receiverdata = self::getReceivers($object, $object_data['actor'], $object_data['tags'], true);
$receivers = $reception_types = [];
foreach ($receiverdata as $key => $data) {
$receivers[$key] = $data['uid'];
$reception_types[$data['uid']] = $data['type'] ?? 0;
}
$object_data['receiver'] = $receivers;
$object_data['reception_type'] = $reception_types;
$object_data['unlisted'] = in_array(-1, $object_data['receiver']); $object_data['unlisted'] = in_array(-1, $object_data['receiver']);
unset($object_data['receiver']['uid:-1']); unset($object_data['receiver']['uid:-1']);

View file

@ -61,6 +61,68 @@ require_once 'mod/share.php';
*/ */
class Transmitter class Transmitter
{ {
/**
* Add relay servers to the list of inboxes
*
* @param array $inboxes
* @return array inboxes with added relay servers
*/
public static function addRelayServerInboxes(array $inboxes)
{
$contacts = DBA::select('apcontact', ['inbox'],
["`type` = ? AND `url` IN (SELECT `url` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?))",
'Application', 0, Contact::FOLLOWER, Contact::FRIEND]);
while ($contact = DBA::fetch($contacts)) {
$inboxes[] = $contact['inbox'];
}
DBA::close($contacts);
return $inboxes;
}
/**
* Subscribe to a relay
*
* @param string $url Subscribe actor url
* @return bool success
*/
public static function sendRelayFollow(string $url)
{
$contact_id = Contact::getIdForURL($url);
if (!$contact_id) {
return false;
}
$activity_id = ActivityPub\Transmitter::activityIDFromContact($contact_id);
$success = ActivityPub\Transmitter::sendActivity('Follow', $url, 0, $activity_id);
if ($success) {
DBA::update('contact', ['rel' => Contact::FRIEND], ['id' => $contact_id]);
}
return $success;
}
/**
* Unsubscribe from a relay
*
* @param string $url Subscribe actor url
* @return bool success
*/
public static function sendRelayUndoFollow(string $url)
{
$contact_id = Contact::getIdForURL($url);
if (!$contact_id) {
return false;
}
$success = self::sendContactUndo($url, $contact_id, 0);
if ($success) {
DBA::update('contact', ['rel' => Contact::SHARING], ['id' => $contact_id]);
}
return $success;
}
/** /**
* Collects a list of contacts of the given owner * Collects a list of contacts of the given owner
* *
@ -793,6 +855,9 @@ class Transmitter
public static function createActivityFromMail($mail_id, $object_mode = false) public static function createActivityFromMail($mail_id, $object_mode = false)
{ {
$mail = self::ItemArrayFromMail($mail_id); $mail = self::ItemArrayFromMail($mail_id);
if (empty($mail)) {
return [];
}
$object = self::createNote($mail); $object = self::createNote($mail);
if (!$object_mode) { if (!$object_mode) {
@ -1917,18 +1982,19 @@ class Transmitter
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException * @throws \ImagickException
* @throws \Exception * @throws \Exception
* @return bool success
*/ */
public static function sendContactUndo($target, $cid, $uid) public static function sendContactUndo($target, $cid, $uid)
{ {
$profile = APContact::getByURL($target); $profile = APContact::getByURL($target);
if (empty($profile['inbox'])) { if (empty($profile['inbox'])) {
Logger::warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); Logger::warning('No inbox found for target', ['target' => $target, 'profile' => $profile]);
return; return false;
} }
$object_id = self::activityIDFromContact($cid); $object_id = self::activityIDFromContact($cid);
if (empty($object_id)) { if (empty($object_id)) {
return; return false;
} }
$id = DI::baseUrl() . '/activity/' . System::createGUID(); $id = DI::baseUrl() . '/activity/' . System::createGUID();
@ -1947,7 +2013,7 @@ class Transmitter
Logger::log('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, Logger::DEBUG); Logger::log('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, Logger::DEBUG);
$signed = LDSignature::sign($data, $owner); $signed = LDSignature::sign($data, $owner);
HTTPSignature::transmit($signed, $profile['inbox'], $uid); return HTTPSignature::transmit($signed, $profile['inbox'], $uid);
} }
private static function prependMentions($body, int $uriid) private static function prependMentions($body, int $uriid)

View file

@ -1499,8 +1499,9 @@ class DFRN
$fields = ['id', 'uid', 'url', 'network', 'avatar-date', 'avatar', 'name-date', 'uri-date', 'addr', $fields = ['id', 'uid', 'url', 'network', 'avatar-date', 'avatar', 'name-date', 'uri-date', 'addr',
'name', 'nick', 'about', 'location', 'keywords', 'xmpp', 'bdyear', 'bd', 'hidden', 'contact-type']; 'name', 'nick', 'about', 'location', 'keywords', 'xmpp', 'bdyear', 'bd', 'hidden', 'contact-type'];
$condition = ["`uid` = ? AND `nurl` = ? AND `network` != ?", $condition = ["`uid` = ? AND `nurl` = ? AND `network` != ? AND NOT `pending` AND NOT `blocked` AND `rel` IN (?, ?)",
$importer["importer_uid"], Strings::normaliseLink($author["link"]), Protocol::STATUSNET]; $importer["importer_uid"], Strings::normaliseLink($author["link"]), Protocol::STATUSNET,
Contact::SHARING, Contact::FRIEND];
$contact_old = DBA::selectFirst('contact', $fields, $condition); $contact_old = DBA::selectFirst('contact', $fields, $condition);
if (DBA::isResult($contact_old)) { if (DBA::isResult($contact_old)) {
@ -1512,8 +1513,9 @@ class DFRN
} }
$author["contact-unknown"] = true; $author["contact-unknown"] = true;
$author["contact-id"] = $importer["id"]; $contact = Contact::getByURL($author["link"], null, ["id", "network"]);
$author["network"] = $importer["network"]; $author["contact-id"] = $contact["id"] ?? $importer["id"];
$author["network"] = $contact["network"] ?? $importer["network"];
$onlyfetch = true; $onlyfetch = true;
} }
@ -1766,15 +1768,15 @@ class DFRN
$msg = []; $msg = [];
$msg["uid"] = $importer["importer_uid"]; $msg["uid"] = $importer["importer_uid"];
$msg["from-name"] = $xpath->query("dfrn:sender/dfrn:name/text()", $mail)->item(0)->nodeValue; $msg["from-name"] = XML::getFirstValue($xpath, "dfrn:sender/dfrn:name/text()", $mail);
$msg["from-url"] = $xpath->query("dfrn:sender/dfrn:uri/text()", $mail)->item(0)->nodeValue; $msg["from-url"] = XML::getFirstValue($xpath, "dfrn:sender/dfrn:uri/text()", $mail);
$msg["from-photo"] = $xpath->query("dfrn:sender/dfrn:avatar/text()", $mail)->item(0)->nodeValue; $msg["from-photo"] = XML::getFirstValue($xpath, "dfrn:sender/dfrn:avatar/text()", $mail);
$msg["contact-id"] = $importer["id"]; $msg["contact-id"] = $importer["id"];
$msg["uri"] = $xpath->query("dfrn:id/text()", $mail)->item(0)->nodeValue; $msg["uri"] = XML::getFirstValue($xpath, "dfrn:id/text()", $mail);
$msg["parent-uri"] = $xpath->query("dfrn:in-reply-to/text()", $mail)->item(0)->nodeValue; $msg["parent-uri"] = XML::getFirstValue($xpath, "dfrn:in-reply-to/text()", $mail);
$msg["created"] = DateTimeFormat::utc($xpath->query("dfrn:sentdate/text()", $mail)->item(0)->nodeValue); $msg["created"] = DateTimeFormat::utc(XML::getFirstValue($xpath, "dfrn:sentdate/text()", $mail));
$msg["title"] = $xpath->query("dfrn:subject/text()", $mail)->item(0)->nodeValue; $msg["title"] = XML::getFirstValue($xpath, "dfrn:subject/text()", $mail);
$msg["body"] = $xpath->query("dfrn:content/text()", $mail)->item(0)->nodeValue; $msg["body"] = XML::getFirstValue($xpath, "dfrn:content/text()", $mail);
Mail::insert($msg); Mail::insert($msg);
} }
@ -2534,6 +2536,11 @@ class DFRN
} }
if (in_array($entrytype, [DFRN::REPLY, DFRN::REPLY_RC])) { if (in_array($entrytype, [DFRN::REPLY, DFRN::REPLY_RC])) {
// Will be overwritten for sharing accounts in Item::insert
if (empty($item['post-type']) && ($entrytype == DFRN::REPLY)) {
$item['post-type'] = Item::PT_COMMENT;
}
$posted_id = Item::insert($item); $posted_id = Item::insert($item);
if ($posted_id) { if ($posted_id) {
Logger::log("Reply from contact ".$item["contact-id"]." was stored with id ".$posted_id, Logger::DEBUG); Logger::log("Reply from contact ".$item["contact-id"]." was stored with id ".$posted_id, Logger::DEBUG);

View file

@ -1735,6 +1735,9 @@ class Diaspora
$datarray["owner-link"] = $contact["url"]; $datarray["owner-link"] = $contact["url"];
$datarray["owner-id"] = Contact::getIdForURL($contact["url"], 0); $datarray["owner-id"] = Contact::getIdForURL($contact["url"], 0);
// Will be overwritten for sharing accounts in Item::insert
$datarray['post-type'] = ($datarray["uid"] == 0) ? Item::PT_GLOBAL : Item::PT_COMMENT;
$datarray["guid"] = $guid; $datarray["guid"] = $guid;
$datarray["uri"] = self::getUriFromGuid($author, $guid); $datarray["uri"] = self::getUriFromGuid($author, $guid);
$datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]);
@ -2862,6 +2865,10 @@ class Diaspora
$datarray["protocol"] = Conversation::PARCEL_DIASPORA; $datarray["protocol"] = Conversation::PARCEL_DIASPORA;
$datarray["source"] = $xml; $datarray["source"] = $xml;
if ($datarray["uid"] == 0) {
$datarray["post-type"] = Item::PT_GLOBAL;
}
$datarray["body"] = self::replacePeopleGuid($body, $contact["url"]); $datarray["body"] = self::replacePeopleGuid($body, $contact["url"]);
self::storeMentions($datarray['uri-id'], $text); self::storeMentions($datarray['uri-id'], $text);

View file

@ -494,6 +494,9 @@ class Feed
} }
$item["body"] = HTML::toBBCode($body, $basepath); $item["body"] = HTML::toBBCode($body, $basepath);
// Remove tracking pixels
$item["body"] = preg_replace("/\[img=1x1\]([^\[\]]*)\[\/img\]/Usi", '', $item["body"]);
if (($item["body"] == '') && ($item["title"] != '')) { if (($item["body"] == '') && ($item["title"] != '')) {
$item["body"] = $item["title"]; $item["body"] = $item["title"];
$item["title"] = ''; $item["title"] = '';
@ -533,6 +536,9 @@ class Feed
$replace = true; $replace = true;
} }
$saved_body = $item["body"];
$saved_title = $item["title"];
if ($replace) { if ($replace) {
$item["body"] = trim($item["title"]); $item["body"] = trim($item["title"]);
} }
@ -549,9 +555,24 @@ class Feed
} }
} }
$data = PageInfo::queryUrl($item["plink"], false, $preview, ($contact["fetch_further_information"] == 2), $contact["ffi_keyword_denylist"] ?? '');
// Take the data that was provided by the feed if the query is empty
if (($data['type'] == 'link') && empty($data['title']) && empty($data['text'])) {
$data['title'] = $saved_title;
$item["body"] = $saved_body;
}
$data_text = strip_tags(trim($data['text'] ?? ''));
$item_body = strip_tags(trim($item['body'] ?? ''));
if (!empty($data_text) && (($data_text == $item_body) || strstr($item_body, $data_text))) {
$data['text'] = '';
}
// We always strip the title since it will be added in the page information // We always strip the title since it will be added in the page information
$item["title"] = ""; $item["title"] = "";
$item["body"] = $item["body"] . "\n" . PageInfo::getFooterFromUrl($item["plink"], false, $preview, ($contact["fetch_further_information"] == 2), $contact["ffi_keyword_denylist"] ?? ''); $item["body"] = $item["body"] . "\n" . PageInfo::getFooterFromData($data, false);
$taglist = $contact["fetch_further_information"] == 2 ? PageInfo::getTagsFromUrl($item["plink"], $preview, $contact["ffi_keyword_denylist"] ?? '') : []; $taglist = $contact["fetch_further_information"] == 2 ? PageInfo::getTagsFromUrl($item["plink"], $preview, $contact["ffi_keyword_denylist"] ?? '') : [];
$item["object-type"] = Activity\ObjectType::BOOKMARK; $item["object-type"] = Activity\ObjectType::BOOKMARK;
unset($item["attach"]); unset($item["attach"]);

View file

@ -49,7 +49,7 @@ abstract class MailBuilder
/** @var LoggerInterface */ /** @var LoggerInterface */
protected $logger; protected $logger;
/** @var string */ /** @var string[][] */
protected $headers; protected $headers;
/** @var string */ /** @var string */
@ -76,13 +76,14 @@ abstract class MailBuilder
$hostname = substr($hostname, 0, strpos($hostname, ':')); $hostname = substr($hostname, 0, strpos($hostname, ':'));
} }
$this->headers = ""; $this->headers = [
$this->headers .= "Precedence: list\n"; 'Precedence' => ['list'],
$this->headers .= "X-Friendica-Host: " . $hostname . "\n"; 'X-Friendica-Host' => [$hostname],
$this->headers .= "X-Friendica-Platform: " . FRIENDICA_PLATFORM . "\n"; 'X-Friendica-Platform' => [FRIENDICA_PLATFORM],
$this->headers .= "X-Friendica-Version: " . FRIENDICA_VERSION . "\n"; 'X-Friendica-Version' => [FRIENDICA_VERSION],
$this->headers .= "List-ID: <notification." . $hostname . ">\n"; 'List-ID' => ['<notification.' . $hostname . '>'],
$this->headers .= "List-Archive: <" . $baseUrl->get() . "/notifications/system>\n"; 'List-Archive' => ['<' . $baseUrl->get() . '/notifications/system>'],
];
} }
/** /**
@ -159,15 +160,31 @@ abstract class MailBuilder
} }
/** /**
* Adds new headers to the default headers * Adds a value to a header
* *
* @param string $headers New headers * @param string $name The header name
* @param string $value The value of the header to add
* *
* @return static * @return static
*/ */
public function addHeaders(string $headers) public function addHeader(string $name, string $value)
{ {
$this->headers .= $headers; $this->headers[$name][] = $value;
return $this;
}
/**
* Sets a value to a header (overwrites existing values)
*
* @param string $name The header name
* @param string $value The value to set
*
* @return static
*/
public function setHeader(string $name, string $value)
{
$this->headers[$name] = [$value];
return $this; return $this;
} }

View file

@ -151,7 +151,7 @@ class Emailer
. rand(10000, 99999); . rand(10000, 99999);
// generate a multipart/alternative message header // generate a multipart/alternative message header
$messageHeader = $email->getAdditionalMailHeader() . $messageHeader = $email->getAdditionalMailHeaderString() .
"From: $fromName <{$fromAddress}>\n" . "From: $fromName <{$fromAddress}>\n" .
"Reply-To: $fromName <{$replyTo}>\n" . "Reply-To: $fromName <{$replyTo}>\n" .
"MIME-Version: 1.0\n" . "MIME-Version: 1.0\n" .

View file

@ -39,6 +39,7 @@ use Friendica\App;
use Friendica\Core\Config\IConfig; use Friendica\Core\Config\IConfig;
use Friendica\Core\PConfig\IPConfig; use Friendica\Core\PConfig\IPConfig;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\DI;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;

View file

@ -177,6 +177,35 @@ class Network
return false; return false;
} }
/**
* Checks if the provided url is on the list of domains where redirects are blocked.
* Returns true if it is or malformed URL, false if not.
*
* @param string $url The url to check the domain from
*
* @return boolean
*/
public static function isRedirectBlocked(string $url)
{
$host = @parse_url($url, PHP_URL_HOST);
if (!$host) {
return false;
}
$no_redirect_list = DI::config()->get('system', 'no_redirect_list', []);
if (!$no_redirect_list) {
return false;
}
foreach ($no_redirect_list as $no_redirect) {
if (fnmatch(strtolower($no_redirect), strtolower($host))) {
return true;
}
}
return false;
}
/** /**
* Check if email address is allowed to register here. * Check if email address is allowed to register here.
* *

View file

@ -68,6 +68,7 @@ class Strings
* *
* @param string $string Input string * @param string $string Input string
* @return string Filtered string * @return string Filtered string
* @deprecated since 2020.09 Please use Smarty default HTML escaping for templates or htmlspecialchars() otherwise
*/ */
public static function escapeTags($string) public static function escapeTags($string)
{ {

View file

@ -488,6 +488,21 @@ class XML
return $first_item->attributes; return $first_item->attributes;
} }
public static function getFirstValue($xpath, $search, $context)
{
$result = $xpath->query($search, $context);
if (!is_object($result)) {
return '';
}
$first_item = $result->item(0);
if (!is_object($first_item)) {
return '';
}
return $first_item->nodeValue;
}
/** /**
* escape text ($str) for XML transport * escape text ($str) for XML transport
* *

View file

@ -52,6 +52,7 @@ class MergeContact
// These fields only contain public contact entries (uid = 0) // These fields only contain public contact entries (uid = 0)
if ($uid == 0) { if ($uid == 0) {
DBA::update('post-tag', ['cid' => $new_cid], ['cid' => $old_cid]); DBA::update('post-tag', ['cid' => $new_cid], ['cid' => $old_cid]);
DBA::delete('post-tag', ['cid' => $old_cid]);
DBA::update('item', ['author-id' => $new_cid], ['author-id' => $old_cid]); DBA::update('item', ['author-id' => $new_cid], ['author-id' => $old_cid]);
DBA::update('item', ['owner-id' => $new_cid], ['owner-id' => $old_cid]); DBA::update('item', ['owner-id' => $new_cid], ['owner-id' => $old_cid]);
DBA::update('thread', ['author-id' => $new_cid], ['author-id' => $old_cid]); DBA::update('thread', ['author-id' => $new_cid], ['author-id' => $old_cid]);

View file

@ -179,7 +179,7 @@ class Notifier
// Only deliver threaded replies (comment to a comment) to Diaspora // Only deliver threaded replies (comment to a comment) to Diaspora
// when the original comment author does support the Diaspora protocol. // when the original comment author does support the Diaspora protocol.
if ($target_item['parent-uri'] != $target_item['thr-parent']) { if ($thr_parent['author-link'] && $target_item['parent-uri'] != $target_item['thr-parent']) {
$diaspora_delivery = Diaspora::isSupportedByContactUrl($thr_parent['author-link']); $diaspora_delivery = Diaspora::isSupportedByContactUrl($thr_parent['author-link']);
Logger::info('Threaded comment', ['diaspora_delivery' => (int)$diaspora_delivery]); Logger::info('Threaded comment', ['diaspora_delivery' => (int)$diaspora_delivery]);
} }
@ -786,6 +786,11 @@ class Notifier
if ($target_item['origin']) { if ($target_item['origin']) {
$inboxes = ActivityPub\Transmitter::fetchTargetInboxes($target_item, $uid); $inboxes = ActivityPub\Transmitter::fetchTargetInboxes($target_item, $uid);
if (in_array($target_item['private'], [Item::PUBLIC])) {
$inboxes = ActivityPub\Transmitter::addRelayServerInboxes($inboxes);
}
Logger::log('Origin item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . ' will be distributed.', Logger::DEBUG); Logger::log('Origin item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . ' will be distributed.', Logger::DEBUG);
} elseif (Item::isForumPost($target_item, $owner)) { } elseif (Item::isForumPost($target_item, $owner)) {
$inboxes = ActivityPub\Transmitter::fetchTargetInboxes($target_item, $uid, false, 0, true); $inboxes = ActivityPub\Transmitter::fetchTargetInboxes($target_item, $uid, false, 0, true);

View file

@ -54,7 +54,7 @@
use Friendica\Database\DBA; use Friendica\Database\DBA;
if (!defined('DB_UPDATE_VERSION')) { if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1367); define('DB_UPDATE_VERSION', 1368);
} }
return [ return [
@ -857,6 +857,7 @@ return [
"indexes" => [ "indexes" => [
"PRIMARY" => ["id"], "PRIMARY" => ["id"],
"uri-plink-hash" => ["UNIQUE", "uri-plink-hash"], "uri-plink-hash" => ["UNIQUE", "uri-plink-hash"],
"title-content-warning-body" => ["FULLTEXT", "title", "content-warning", "body"],
"uri" => ["uri(191)"], "uri" => ["uri(191)"],
"plink" => ["plink(191)"], "plink" => ["plink(191)"],
"uri-id" => ["uri-id"] "uri-id" => ["uri-id"]

View file

@ -210,6 +210,10 @@ return [
// Disable the polling of DFRN and OStatus contacts through onepoll.php. // Disable the polling of DFRN and OStatus contacts through onepoll.php.
'disable_polling' => false, 'disable_polling' => false,
// display_resharer (Boolean)
// Display the first resharer as icon and text on a reshared item.
'display_resharer' => false,
// dlogfile (Path) // dlogfile (Path)
// location of the developer log file. // location of the developer log file.
'dlogfile' => '', 'dlogfile' => '',
@ -303,6 +307,12 @@ return [
// Maximum number of queue items for a single contact before subsequent messages are discarded. // Maximum number of queue items for a single contact before subsequent messages are discarded.
'max_contact_queue' => 500, 'max_contact_queue' => 500,
// max_csv_file_size (Integer)
// When uploading a CSV with account addresses to follow
// in the user settings, this controls the maximum file
// size of the upload file.
'max_csv_file_size' => 30720,
// max_feed_items (Integer) // max_feed_items (Integer)
// Maximum number of feed items that are fetched and processed. For unlimited items set to 0. // Maximum number of feed items that are fetched and processed. For unlimited items set to 0.
'max_feed_items' => 20, 'max_feed_items' => 20,
@ -352,6 +362,10 @@ return [
// Don't use OEmbed to fetch more information about a link. // Don't use OEmbed to fetch more information about a link.
'no_oembed' => false, 'no_oembed' => false,
// no_redirect_list (Array)
// List of domains where HTTP redirects should be ignored.
'no_redirect_list' => [],
// no_smilies (Boolean) // no_smilies (Boolean)
// Don't show smilies. // Don't show smilies.
'no_smilies' => false, 'no_smilies' => false,
@ -395,10 +409,6 @@ return [
// - 0 = every minute // - 0 = every minute
'pushpoll_frequency' => 3, 'pushpoll_frequency' => 3,
// queue_no_dead_check (Boolean)
// Ignore if the target contact or server seems to be dead during queue delivery.
'queue_no_dead_check' => false,
// redis_host (String) // redis_host (String)
// Host name of the redis daemon. // Host name of the redis daemon.
'redis_host' => '127.0.0.1', 'redis_host' => '127.0.0.1',

Some files were not shown because too many files have changed in this diff Show more