Browse Source

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

# Conflicts:
#	composer.lock
tags/2020.09-1
Hypolite Petovan 1 month ago
parent
commit
a852455d0e
100 changed files with 1519 additions and 762 deletions
  1. +2
    -2
      .gitignore
  2. +3
    -0
      .htaccess-dist
  3. +76
    -2
      CHANGELOG
  4. +5
    -4
      CREDITS.txt
  5. +1
    -1
      VERSION
  6. +10
    -0
      bin/.htaccess
  7. +5
    -0
      bin/auth_ejabberd.php
  8. +5
    -0
      bin/console.php
  9. +5
    -0
      bin/daemon.php
  10. +1
    -1
      bin/dev/make_credits.py
  11. +4
    -0
      bin/testargs.php
  12. +5
    -0
      bin/wait-for-connection
  13. +5
    -0
      bin/worker.php
  14. +1
    -33
      boot.php
  15. +1
    -1
      composer.json
  16. +54
    -50
      composer.lock
  17. +3
    -0
      database.sql
  18. +30
    -33
      doc/Forums.md
  19. +3
    -2
      doc/Install.md
  20. +1
    -0
      doc/tools.md
  21. +5
    -5
      include/api.php
  22. +68
    -72
      include/conversation.php
  23. +11
    -7
      include/enotify.php
  24. +1
    -1
      mod/editpost.php
  25. +5
    -5
      mod/events.php
  26. +9
    -41
      mod/item.php
  27. +0
    -30
      mod/message.php
  28. +1
    -3
      mod/network.php
  29. +1
    -1
      mod/notes.php
  30. +34
    -9
      mod/photos.php
  31. +0
    -20
      mod/settings.php
  32. +3
    -0
      mods/sample-Lighttpd.config
  33. +5
    -0
      mods/sample-nginx.config
  34. +21
    -68
      src/App/Arguments.php
  35. +10
    -1
      src/Console/DatabaseStructure.php
  36. +134
    -0
      src/Console/Relay.php
  37. +12
    -8
      src/Content/PageInfo.php
  38. +1
    -1
      src/Content/Pager.php
  39. +21
    -0
      src/Core/ACL.php
  40. +0
    -2
      src/Core/Addon.php
  41. +2
    -0
      src/Core/Console.php
  42. +14
    -14
      src/Core/Worker.php
  43. +8
    -0
      src/Database/DBA.php
  44. +16
    -0
      src/Database/DBStructure.php
  45. +10
    -1
      src/Factory/Api/Mastodon/Status.php
  46. +8
    -3
      src/Model/APContact.php
  47. +1
    -1
      src/Model/Contact.php
  48. +32
    -23
      src/Model/Item.php
  49. +29
    -0
      src/Model/ItemContent.php
  50. +5
    -2
      src/Model/Mail.php
  51. +2
    -12
      src/Model/Profile.php
  52. +15
    -0
      src/Model/Tag.php
  53. +16
    -4
      src/Model/UserItem.php
  54. +2
    -2
      src/Module/Admin/Blocklist/Contact.php
  55. +13
    -19
      src/Module/Admin/Site.php
  56. +5
    -5
      src/Module/Admin/Users.php
  57. +4
    -4
      src/Module/Api/Mastodon/Timelines/PublicTimeline.php
  58. +4
    -4
      src/Module/Api/Twitter/ContactEndpoint.php
  59. +1
    -1
      src/Module/BaseAdmin.php
  60. +3
    -3
      src/Module/BaseApi.php
  61. +3
    -15
      src/Module/Contact.php
  62. +1
    -1
      src/Module/Friendica.php
  63. +1
    -1
      src/Module/Group.php
  64. +1
    -1
      src/Module/Invite.php
  65. +12
    -1
      src/Module/Profile/Status.php
  66. +19
    -22
      src/Module/Search/Index.php
  67. +1
    -1
      src/Module/Security/TwoFactor/Recovery.php
  68. +1
    -1
      src/Module/Settings/Profile/Photo/Crop.php
  69. +3
    -3
      src/Module/Settings/TwoFactor/AppSpecific.php
  70. +1
    -1
      src/Module/Settings/TwoFactor/Index.php
  71. +1
    -1
      src/Module/Settings/TwoFactor/Recovery.php
  72. +1
    -1
      src/Module/Settings/TwoFactor/Verify.php
  73. +7
    -2
      src/Network/HTTPRequest.php
  74. +4
    -2
      src/Network/Probe.php
  75. +55
    -0
      src/Object/Api/Mastodon/Activity.php
  76. +46
    -0
      src/Object/Api/Mastodon/Application.php
  77. +5
    -4
      src/Object/Api/Mastodon/Status.php
  78. +56
    -0
      src/Object/Api/Mastodon/Status/Counts.php
  79. +5
    -5
      src/Object/Api/Twitter/User.php
  80. +9
    -2
      src/Object/EMail/IEmail.php
  81. +23
    -4
      src/Object/Email.php
  82. +31
    -44
      src/Object/Post.php
  83. +42
    -41
      src/Protocol/ActivityPub/Processor.php
  84. +223
    -73
      src/Protocol/ActivityPub/Receiver.php
  85. +69
    -3
      src/Protocol/ActivityPub/Transmitter.php
  86. +19
    -12
      src/Protocol/DFRN.php
  87. +7
    -0
      src/Protocol/Diaspora.php
  88. +22
    -1
      src/Protocol/Feed.php
  89. +29
    -12
      src/Util/EMailer/MailBuilder.php
  90. +1
    -1
      src/Util/Emailer.php
  91. +1
    -0
      src/Util/ExAuth.php
  92. +29
    -0
      src/Util/Network.php
  93. +0
    -0
      src/Util/PidFile.php
  94. +1
    -0
      src/Util/Strings.php
  95. +15
    -0
      src/Util/XML.php
  96. +0
    -0
      src/Worker/CleanWorkerQueue.php
  97. +1
    -0
      src/Worker/MergeContact.php
  98. +6
    -1
      src/Worker/Notifier.php
  99. +2
    -1
      static/dbstructure.config.php
  100. +14
    -4
      static/defaults.config.php

+ 2
- 2
.gitignore View File

@@ -71,8 +71,8 @@ venv/
/addons
/addon

#ignore .htaccess
.htaccess
#ignore base .htaccess
/.htaccess

#ignore filesystem storage default path
/storage


+ 3
- 0
.htaccess-dist 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
AddType application/x-java-archive .jar
AddType audio/ogg .oga


+ 76
- 2
CHANGELOG 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)
Friendica Core:
@@ -668,7 +742,7 @@ Version 2018.09 (2018-09-23)
Version 2018.05 (2018-06-01)
Friendica Core:
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 relay system [annando]
Enhancements to the handling of URL that contain unicode characters [annando]


+ 5
- 4
CREDITS.txt View File

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


+ 1
- 1
VERSION View File

@@ -1 +1 @@
2020.09-dev
2020.09

+ 10
- 0
bin/.htaccess 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>

+ 5
- 0
bin/auth_ejabberd.php View File

@@ -51,6 +51,11 @@
*
*/

if (php_sapi_name() !== 'cli') {
header($_SERVER["SERVER_PROTOCOL"] . ' 403 Forbidden');
exit();
}

use Dice\Dice;
use Friendica\App\Mode;
use Friendica\Util\ExAuth;


+ 5
- 0
bin/console.php View File

@@ -20,6 +20,11 @@
*
*/

if (php_sapi_name() !== 'cli') {
header($_SERVER["SERVER_PROTOCOL"] . ' 403 Forbidden');
exit();
}

use Dice\Dice;
use Psr\Log\LoggerInterface;



+ 5
- 0
bin/daemon.php View File

@@ -23,6 +23,11 @@
* 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 Friendica\Core\Logger;
use Friendica\Core\Worker;


+ 1
- 1
bin/dev/make_credits.py 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])
print('> base directory is assumed to be: '+path)
# 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
print('> getting contributors to the friendica core repository')
p = subprocess.Popen(['git', 'shortlog', '--no-merges', '-s'],


+ 4
- 0
bin/testargs.php 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])) {
echo $_SERVER["argv"][1];


+ 5
- 0
bin/wait-for-connection View File

@@ -24,6 +24,11 @@
* Usage: php bin/wait-for-connection {HOST} {PORT} [{TIMEOUT}]
*/

if (php_sapi_name() !== 'cli') {
header($_SERVER["SERVER_PROTOCOL"] . ' 403 Forbidden');
exit();
}

$timeout = 60;
switch ($argc) {
case 4:


+ 5
- 0
bin/worker.php View File

@@ -21,6 +21,11 @@
* Starts the background processing
*/

if (php_sapi_name() !== 'cli') {
header($_SERVER["SERVER_PROTOCOL"] . ' 403 Forbidden');
exit();
}

use Dice\Dice;
use Friendica\App;
use Friendica\Core\Process;


+ 1
- 33
boot.php View File

@@ -38,7 +38,7 @@ use Friendica\Util\DateTimeFormat;

define('FRIENDICA_PLATFORM', 'Friendica');
define('FRIENDICA_CODENAME', 'Red Hot Poker');
define('FRIENDICA_VERSION', '2020.09-dev');
define('FRIENDICA_VERSION', '2020.09');
define('DFRN_PROTOCOL_VERSION', '2.23');
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);
}

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
*


+ 1
- 1
composer.json View File

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


+ 54
- 50
composer.lock View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ffe94190e166cebf80601fc3d6d26be0",
"content-hash": "ed9aa898eaf8a1f8a807f3be9eecc3d7",
"packages": [
{
"name": "asika/simple-console",
@@ -3487,55 +3487,6 @@
],
"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",
"version": "v1.1.0",
@@ -3740,6 +3691,59 @@
],
"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",
"version": "1.0.1",


+ 3
- 0
database.sql View File

@@ -1,6 +1,6 @@
-- ------------------------------------------
-- Friendica 2020.09-rc (Red Hot Poker)
-- DB_UPDATE_VERSION 1368
-- ------------------------------------------


@@ -786,6 +786,7 @@ CREATE TABLE IF NOT EXISTS `item-content` (
`verb` varchar(100) NOT NULL DEFAULT '' COMMENT 'ActivityStreams verb',
PRIMARY KEY(`id`),
UNIQUE INDEX `uri-plink-hash` (`uri-plink-hash`),
FULLTEXT INDEX `title-content-warning-body` (`title`,`content-warning`,`body`),
INDEX `uri` (`uri`(191)),
INDEX `plink` (`plink`(191)),
INDEX `uri-id` (`uri-id`),


+ 30
- 33
doc/Forums.md View File

@@ -4,49 +4,46 @@ Forums
* [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.
This applies to all forums, whether they are normal profiles or forum profiles.
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.

Therefore the first thing you need to do to create a new forum is to register a new account for the forum.
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.
Managing Accounts
---

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.
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.
Here you can register additional accounts with new nicknames that will be linked to your primary account.

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.
You may appoint a delegate to manage your new account (e.g. forum page).
The Delegates section of Manage Accounts page will provide you with a list of contacts on this instance under "Potential Delegates".
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.
Please use this facility wisely.
Delegated managers will not be able to alter basic account settings, such as passwords or page types, or remove the account.

The "Automatic Friend Account" is typically used for personal profile forums where you wish to automatically approve any friendship/connection requests.
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.

Managing Multiple forums
Types of Accounts
---

We recommend that you create group forums with the same email address and password as your normal account.
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.
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.
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.

You may also appoint a delegate to manage your forum.
Do this by visiting the [Delegation Setup Page](settings/delegation).
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.
They will be able to edit contacts, profiles, and all content for this account/page.
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.
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
---


+ 3
- 2
doc/Install.md 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
* 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
* 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)
* 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.)
@@ -47,7 +47,6 @@ For alternative server configurations (such as Nginx server and MariaDB database
### Optional

* PHP ImageMagick extension (php-imagick) for animated GIF support.
* [Composer](https://getcomposer.org/) for a git install

## Installation procedure

@@ -61,6 +60,8 @@ If this is nothing for you, you might be interested in

### 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.

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.


+ 1
- 0
doc/tools.md View File

@@ -27,6 +27,7 @@ The console provides the following commands:
* typo: Checks for parse errors in Friendica files
* postupdate: Execute pending post update scripts (can last days)
* 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.



+ 5
- 5
include/api.php View File

@@ -311,22 +311,22 @@ function api_call(App $a, App\Arguments $args = null)
}

$type = "json";
if (strpos($args->getQueryString(), ".xml") > 0) {
if (strpos($args->getCommand(), ".xml") > 0) {
$type = "xml";
}
if (strpos($args->getQueryString(), ".json") > 0) {
if (strpos($args->getCommand(), ".json") > 0) {
$type = "json";
}
if (strpos($args->getQueryString(), ".rss") > 0) {
if (strpos($args->getCommand(), ".rss") > 0) {
$type = "rss";
}
if (strpos($args->getQueryString(), ".atom") > 0) {
if (strpos($args->getCommand(), ".atom") > 0) {
$type = "atom";
}

try {
foreach ($API as $p => $info) {
if (strpos($args->getQueryString(), $p) === 0) {
if (strpos($args->getCommand(), $p) === 0) {
if (!api_check_method($info['method'])) {
throw new MethodNotAllowedException();
}


+ 68
- 72
include/conversation.php View File

@@ -520,10 +520,6 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o

$threadsid++;

$owner_url = '';
$owner_name = '';
$sparkle = '';

// prevent private email from leaking.
if ($item['network'] === Protocol::MAIL && local_user() != $item['uid']) {
continue;
@@ -540,14 +536,14 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
'network' => $item['author-network'], 'url' => $item['author-link']];
$profile_link = Contact::magicLinkByContact($author);

$sparkle = '';
if (strpos($profile_link, 'redir/') === 0) {
$sparkle = ' sparkle';
}

$locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => ''];
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'] ?: '');

localize_item($item);
if ($mode === 'network-new') {
@@ -563,10 +559,6 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
'delete' => DI::l10n()->t('Delete'),
];

$star = false;
$isstarred = "unstarred";

$lock = false;
$likebuttons = [
'like' => null,
'dislike' => null,
@@ -577,7 +569,7 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
unset($likebuttons['dislike']);
}

$body = Item::prepareBody($item, true, $preview);
$body_html = Item::prepareBody($item, true, $preview);

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']),
'linktitle' => DI::l10n()->t('View %s\'s profile @ %s', $profile_name, $item['author-link']),
'profile_url' => $profile_link,
'item_photo_menu' => item_photo_menu($item),
'item_photo_menu_html' => item_photo_menu($item),
'name' => $profile_name,
'sparkle' => $sparkle,
'lock' => $lock,
'lock' => false,
'thumb' => DI::baseUrl()->remove($item['author-avatar']),
'title' => $title,
'body' => $body,
'body_html' => $body_html,
'tags' => $tags['tags'],
'hashtags' => $tags['hashtags'],
'mentions' => $tags['mentions'],
@@ -613,23 +605,23 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
'has_folders' => ((count($folders)) ? 'true' : ''),
'categories' => $categories,
'folders' => $folders,
'text' => strip_tags($body),
'text' => strip_tags($body_html),
'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'])),
'location' => $location,
'location_html' => $location_html,
'indent' => '',
'owner_name' => $owner_name,
'owner_url' => $owner_url,
'owner_name' => '',
'owner_url' => '',
'owner_photo' => DI::baseUrl()->remove($item['owner-avatar']),
'plink' => Item::getPlink($item),
'edpost' => false,
'isstarred' => $isstarred,
'star' => $star,
'isstarred' => 'unstarred',
'star' => false,
'drop' => $drop,
'vote' => $likebuttons,
'like' => '',
'dislike' => '',
'comment' => '',
'like_html' => '',
'dislike_html' => '',
'comment_html' => '',
'conv' => (($preview) ? '' : ['href'=> 'display/'.$item['guid'], 'title'=> DI::l10n()->t('View in context')]),
'previewing' => $previewing,
'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)
&& Contact::isSharing($row['author-id'], $row['uid'])) {
$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'];
}

@@ -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'])];
}

if (($row['gravity'] == GRAVITY_PARENT) && !$row['origin'] && ($row['author-id'] == $row['owner-id'])
&& !Contact::isSharing($row['author-id'], $row['uid'])) {
if ($row['post-type'] == Item::PT_TAG) {
switch ($row['post-type']) {
case Item::PT_TO:
$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')];
}
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;
}

@@ -758,10 +788,13 @@ function conversation_fetch_comments($thread_items, $pinned) {
if (!empty($direction)) {
foreach ($parentlines as $line) {
$comments[$line]['direction'] = $direction;
if (!empty($actor) && DI::pConfig()->get(local_user(), 'system', 'display_resharer') ) {
$comments[$line]['owner-link'] = $actor['link'];
$comments[$line]['owner-avatar'] = $actor['avatar'];
$comments[$line]['owner-name'] = $actor['name'];
if (!empty($actor)) {
$comments[$line]['reshared'] = DI::l10n()->t('%s reshared this.', $actor['url']);
if (DI::pConfig()->get(local_user(), 'system', 'display_resharer') ) {
$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 = '';
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::getMarkupTemplate("jot.tpl");

$o .= Renderer::replaceMacros($tpl,[
'$new_post' => DI::l10n()->t('New Post'),
'$return_path' => $query_str,
'$return_path' => DI::args()->getQueryString(),
'$action' => 'item',
'$share' => ($x['button'] ?? '') ?: DI::l10n()->t('Share'),
'$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") : '',
'$wait' => DI::l10n()->t('Please wait'),
'$permset' => DI::l10n()->t('Permission settings'),
'$shortpermset' => DI::l10n()->t('permissions'),
'$shortpermset' => DI::l10n()->t('Permissions'),
'$wall' => $notes_cid ? 0 : 1,
'$posttype' => $notes_cid ? Item::PT_PERSONAL_NOTE : Item::PT_ARTICLE,
'$content' => $x['content'] ?? '',
@@ -1212,11 +1223,6 @@ function status_editor(App $a, $x, $notes_cid = 0, $popup = false)

// ACL permissions box
'$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)
'$message' => DI::l10n()->t('Message'),
@@ -1490,13 +1496,3 @@ function sort_thr_commented(array $a, array $b)
{
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'];
}
}

+ 11
- 7
include/enotify.php View File

@@ -87,12 +87,15 @@ function notification($params)
}
$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
// the database, and an email is sent if applicable.
// default, if not specified: 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)) {
$title = $params['item']['title'];
@@ -509,7 +512,8 @@ function notification($params)
Logger::log('sending notification email');

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?
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];
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" .
" parent: ${params['parent']}\n" . " uid : ${params['uid']}\n";
Logger::log($log_msg, Logger::DEBUG);
} else {
// 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);
}
}
@@ -545,7 +550,6 @@ function notification($params)
'title' => $title,
'body' => $body,
'subject' => $subject,
'headers' => $additional_mail_header,
];

Hook::callAll('enotify_mail', $datarray);
@@ -564,13 +568,13 @@ function notification($params)

// If a photo is present, add it to the email
if (!empty($datarray['source_photo'])) {
$builder->withPhoto(
$emailBuilder->withPhoto(
$datarray['source_photo'],
$datarray['source_link'] ?? $sitelink,
$datarray['source_name'] ?? $sitename);
}

$email = $builder->build();
$email = $emailBuilder->build();

// use the Emailer class to send the message
return DI::emailer()->send($email);


+ 1
- 1
mod/editpost.php View File

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

'$compose_link_title' => DI::l10n()->t('Open Compose page'),
]);


+ 5
- 5
mod/events.php View File

@@ -474,16 +474,16 @@ function events_content(App $a)
$t_orig = $orig_event['summary'] ?? '';
$d_orig = $orig_event['desc'] ?? '';
$l_orig = $orig_event['location'] ?? '';
$eid = !empty($orig_event) ? $orig_event['id'] : 0;
$cid = !empty($orig_event) ? $orig_event['cid'] : 0;
$uri = !empty($orig_event) ? $orig_event['uri'] : '';
$eid = $orig_event['id'] ?? 0;
$cid = $orig_event['cid'] ?? 0;
$uri = $orig_event['uri'] ?? '';

if ($cid || $mode === 'edit') {
$share_disabled = 'disabled="disabled"';
}

$sdt = !empty($orig_event) ? $orig_event['start'] : 'now';
$fdt = !empty($orig_event) ? $orig_event['finish'] : 'now';
$sdt = $orig_event['start'] ?? 'now';
$fdt = $orig_event['finish'] ?? 'now';

$tz = date_default_timezone_get();
if (!empty($orig_event)) {


+ 9
- 41
mod/item.php View File

@@ -260,7 +260,7 @@ function item_post(App $a) {
$objecttype = $orig_post['object-type'];
$app = $orig_post['app'];
$categories = $orig_post['file'] ?? '';
$title = Strings::escapeTags(trim($_REQUEST['title']));
$title = trim($_REQUEST['title'] ?? '');
$body = trim($body);
$private = $orig_post['private'];
$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'] ?? '';
}

$title = Strings::escapeTags(trim($_REQUEST['title'] ?? ''));
$location = Strings::escapeTags(trim($_REQUEST['location'] ?? ''));
$coord = Strings::escapeTags(trim($_REQUEST['coord'] ?? ''));
$verb = Strings::escapeTags(trim($_REQUEST['verb'] ?? ''));
$emailcc = Strings::escapeTags(trim($_REQUEST['emailcc'] ?? ''));
$title = trim($_REQUEST['title'] ?? '');
$location = trim($_REQUEST['location'] ?? '');
$coord = trim($_REQUEST['coord'] ?? '');
$verb = trim($_REQUEST['verb'] ?? '');
$emailcc = trim($_REQUEST['emailcc'] ?? '');
$body = trim($body);
$network = Strings::escapeTags(trim(($_REQUEST['network'] ?? '') ?: Protocol::DFRN));
$network = trim(($_REQUEST['network'] ?? '') ?: Protocol::DFRN);
$guid = System::createUUID();

$postopts = $_REQUEST['postopts'] ?? '';
@@ -904,40 +904,8 @@ function drop_item(int $id, string $return = '')
}

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'])) {
$fields = ['guid'];
$parentitem = Item::selectFirstForUser(local_user(), $fields, ['id' => $item['parent']]);
$parentitem = Item::selectFirstForUser(local_user(), ['guid'], ['id' => $item['parent']]);
}

// delete the item
@@ -949,7 +917,7 @@ function drop_item(int $id, string $return = '')
$return_url = str_replace("update_", "", $return_url);

// Check if delete a comment
if ($is_comment) {
if ($item['gravity'] == GRAVITY_COMMENT) {
// Return to parent guid
if (!empty($parentitem)) {
DI::baseUrl()->redirect('display/' . $parentitem['guid']);


+ 0
- 30
mod/message.php View File

@@ -141,36 +141,6 @@ function message_content(App $a)
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];
if ($cmd === 'drop') {
$message = DBA::selectFirst('mail', ['convid'], ['id' => $a->argv[2], 'uid' => local_user()]);


+ 1
- 3
mod/network.php View File

@@ -635,9 +635,7 @@ function network_display_post($a, $pager, $mark_all, $update, $ordering, $items)
$parents_str = implode(', ', $parents_arr);
}

$query_string = DI::args()->getQueryString();

$pager->setQueryString($query_string);
$pager->setQueryString(DI::args()->getQueryString());

// 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


+ 1
- 1
mod/notes.php View File

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


+ 34
- 9
mod/photos.php View File

@@ -25,6 +25,7 @@ use Friendica\Content\Nav;
use Friendica\Content\Pager;
use Friendica\Content\Text\BBCode;
use Friendica\Core\ACL;
use Friendica\Core\Addon;
use Friendica\Core\Hook;
use Friendica\Core\Logger;
use Friendica\Core\Renderer;
@@ -987,8 +988,6 @@ function photos_content(App $a)
'$uploadurl' => $ret['post_url'],

// ACL permissions box
'$group_perms' => DI::l10n()->t('Show to Groups'),
'$contact_perms' => DI::l10n()->t('Show to Contacts'),
'$return_path' => DI::args()->getQueryString(),
]);

@@ -1040,7 +1039,6 @@ function photos_content(App $a)
return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [
'$method' => 'post',
'$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_url' => $drop_url,
'$confirm_name' => 'dropalbum',
@@ -1147,7 +1145,6 @@ function photos_content(App $a)
return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [
'$method' => 'post',
'$message' => DI::l10n()->t('Do you really want to delete this photo?'),
'$extra_inputs' => [],
'$confirm' => DI::l10n()->t('Delete Photo'),
'$confirm_url' => $drop_url,
'$confirm_name' => 'delete',
@@ -1352,8 +1349,6 @@ function photos_content(App $a)
'$delete' => DI::l10n()->t('Delete Photo'),

// ACL permissions box
'$group_perms' => DI::l10n()->t('Show to Groups'),
'$contact_perms' => DI::l10n()->t('Show to Contacts'),
'$return_path' => DI::args()->getQueryString(),
]);
}
@@ -1382,6 +1377,16 @@ function photos_content(App $a)

if (!DBA::isResult($items)) {
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, [
'$return_path' => '',
'$jsreload' => $return_path,
@@ -1396,7 +1401,7 @@ function photos_content(App $a)
'$preview' => DI::l10n()->t('Preview'),
'$loading' => DI::l10n()->t('Loading...'),
'$sourceapp' => DI::l10n()->t($a->sourcename),
'$ww' => '',
'$qcomment' => $qcomment,
'$rand_num' => Crypto::randomDigits(12)
]);
}
@@ -1429,6 +1434,16 @@ function photos_content(App $a)
}

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,[
'$return_path' => '',
'$jsreload' => $return_path,
@@ -1442,7 +1457,7 @@ function photos_content(App $a)
'$submit' => DI::l10n()->t('Submit'),
'$preview' => DI::l10n()->t('Preview'),
'$sourceapp' => DI::l10n()->t($a->sourcename),
'$ww' => '',
'$qcomment' => $qcomment,
'$rand_num' => Crypto::randomDigits(12)
]);
}
@@ -1492,6 +1507,16 @@ function photos_content(App $a)
]);

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, [
'$return_path' => '',
'$jsreload' => $return_path,
@@ -1505,7 +1530,7 @@ function photos_content(App $a)
'$submit' => DI::l10n()->t('Submit'),
'$preview' => DI::l10n()->t('Preview'),
'$sourceapp' => DI::l10n()->t($a->sourcename),
'$ww' => '',
'$qcomment' => $qcomment,
'$rand_num' => Crypto::randomDigits(12)
]);
}


+ 0
- 20
mod/settings.php View File

@@ -829,26 +829,6 @@ function settings_content(App $a)

$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 */
$lang_choices = DI::l10n()->getAvailableLanguages();



+ 3
- 0
mods/sample-Lighttpd.config View File

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


+ 5
- 0
mods/sample-nginx.config View File

@@ -141,4 +141,9 @@ server {
location ~ /\. {
deny all;
}

# deny access to the CLI scripts
location ^~ /bin {
deny all;
}
}

+ 21
- 68
src/App/Arguments.php View File

@@ -47,7 +47,7 @@ class Arguments
*/
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->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()
{
@@ -121,50 +121,27 @@ class Arguments
*/
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) {
$queryString = urldecode(substr($server['QUERY_STRING'], 9));
} 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, '/');
$queryParameters = [];
parse_str($server['QUERY_STRING'], $queryParameters);

if (!empty($get['pagename'])) {
$command = trim($get['pagename'], '/\\');
} elseif (!empty($queryParameters['pagename'])) {
$command = trim($queryParameters['pagename'], '/\\');
} elseif (!empty($get['q'])) {
// Legacy page name parameter, now conflicts with the search query parameter
$command = trim($get['q'], '/\\');
} else {
$command = Module::DEFAULT;
$command = '';
}


// fix query_string
if (!empty($command)) {
$queryString = str_replace(
$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);
}
// Remove generated and one-time use parameters
unset($queryParameters['pagename']);
unset($queryParameters['zrl']);
unset($queryParameters['owt']);

/*
* Break the URL path into C style argc/argv style arguments for our
@@ -173,41 +150,17 @@ class Arguments
* [0] => 'module'
* [1] => 'arg1'
* [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);

$queryString = $command . ($queryParameters ? '?' . http_build_query($queryParameters) : '');

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);
}
}
}

+ 10
- 1
src/Console/DatabaseStructure.php View File

@@ -55,6 +55,7 @@ Commands
update Update 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
version Set the database to a given number

Options
-h|--help|-? Show help information
@@ -86,8 +87,10 @@ HELP;
return 0;
}

if (count($this->args) > 1) {
if ((count($this->args) > 1) && ($this->getArgument(0) != 'version')) {
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()) {
@@ -115,6 +118,12 @@ HELP;
DBStructure::convertToInnoDB();
$output = ob_get_clean();
break;
case "version":
ob_start();
DBStructure::setDatabaseVersion($this->getArgument(1));
$output = ob_get_clean();
break;
default:
$output = 'Unknown command: ' . $this->getArgument(0);
}


+ 134
- 0
src/Console/Relay.php 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;
}
}

+ 12
- 8
src/Content/PageInfo.php View File

@@ -134,14 +134,6 @@ class PageInfo

$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'])) {
$text .= " url='" . $data['url'] . "'";
}
@@ -150,6 +142,10 @@ class PageInfo
$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)
if (!empty($data['images']) && !empty($data['images'][0]['width'])) {
$preview = str_replace(['[', ']'], ['&#91;', '&#93;'], htmlentities($data['images'][0]['src'], ENT_QUOTES, 'UTF-8', false));
@@ -160,6 +156,14 @@ class PageInfo
$text .= " image='" . $preview . "'";
} else {
$text .= " preview='" . $preview . "'";

if (empty($data['text'])) {
$data['text'] = $data['title'];
}
if (empty($data['text'])) {
$data['text'] = $data['url'];
}
}
}



+ 1
- 1
src/Content/Pager.php View File

@@ -128,7 +128,7 @@ class Pager
/**
* 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
*/


+ 21
- 0
src/Core/ACL.php View File

@@ -83,6 +83,27 @@ class ACL
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
*


+ 0
- 2
src/Core/Addon.php View File

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

$addon = Strings::sanitizeFilePathItem($addon);

$info = [


+ 2
- 0
src/Core/Console.php View File

@@ -64,6 +64,7 @@ Commands:
postupdate Execute pending post update scripts (can last days)
serverblock Manage blocked servers
storage Manage storage backend
relay Manage ActivityPub relay servers

Options:
-h|--help|-? Show help information
@@ -92,6 +93,7 @@ HELP;
'postupdate' => Friendica\Console\PostUpdate::class,
'serverblock' => Friendica\Console\ServerBlock::class,
'storage' => Friendica\Console\Storage::class,
'relay' => Friendica\Console\Relay::class,
];

/**


+ 14
- 14
src/Core/Worker.php View File

@@ -68,7 +68,7 @@ class Worker

// At first check the maximum load. We shouldn't continue with a high load
if (DI::process()->isMaxLoadReached()) {
Logger::info('Pre check: maximum load reached, quitting.');
Logger::notice('Pre check: maximum load reached, quitting.');
return;
}

@@ -134,7 +134,7 @@ class Worker

// Check free memory
if (DI::process()->isMinMemoryReached()) {
Logger::info('Memory limit reached, quitting.');
Logger::notice('Memory limit reached, quitting.');
DI::lock()->release(self::LOCK_WORKER);
return;
}
@@ -176,19 +176,19 @@ class Worker

// Do we have too few memory?
if (DI::process()->isMinMemoryReached()) {
Logger::info('Memory limit reached, quitting.');
Logger::notice('Memory limit reached, quitting.');
return false;
}

// Possibly there are too much database connections
if (self::maxConnectionsReached()) {
Logger::info('Maximum connections reached, quitting.');
Logger::notice('Maximum connections reached, quitting.');
return false;
}

// Possibly there are too much database processes that block the system
if (DI::process()->isMaxProcessesReached()) {
Logger::info('Maximum processes reached, quitting.');
Logger::notice('Maximum processes reached, quitting.');
return false;
}
@@ -286,25 +286,25 @@ class Worker

// Quit when in maintenance
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;
}

// Constantly check the number of parallel database processes
if (DI::process()->isMaxProcessesReached()) {
Logger::info("Max processes reached for process", ['pid' => $mypid]);
Logger::notice("Max processes reached for process", ['pid' => $mypid]);
return false;
}

// Constantly check the number of available database connections to let the frontend be accessible at any time
if (self::maxConnectionsReached()) {
Logger::info("Max connection reached for process", ['pid' => $mypid]);
Logger::notice("Max connection reached for process", ['pid' => $mypid]);
return false;
}

$argv = json_decode($queue["parameter"], true);
if (empty($argv)) {
Logger::error('Parameter is empty', ['queue' => $queue]);
Logger::warning('Parameter is empty', ['queue' => $queue]);
return false;
}

@@ -348,7 +348,7 @@ class Worker
}

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);
DBA::delete('workerqueue', ['id' => $queue["id"]]);