Merge branch 'master' 2019.12 into develop

This commit is contained in:
Tobias Diekershoff 2019-12-23 20:03:47 +01:00
commit 00756737b5
66 changed files with 70843 additions and 67948 deletions

View file

@ -1,11 +1,59 @@
Version 2019.12-dev (unreleased)
Version 2019.12 (2019-12-23)
Friendica Core:
Updates to the translations (CS, DE, ET, JA, NL, PL) [translation teams]
Updates to the documentation [copiis, MrPetovan, stom79, tobiasd]
Updates to the themes (all) [annando, hoergen, MrPetovan, tobiasd]
General code refactoring and enhancements [annando, MrPetovan, nupplaphil, tobiasd]
Enhanced the manage functionality [annando]
Enhanced the federation with ActivityPub and Diaspora* protocol detection and contact requests [annando]
Enhanced federation with pixelfed and peertube [annando]
Enhanced how the API handles quoted postings [annando]
Enhanced the attachment removal by the API [annando]
Enhanced the 2FA field for mobile devices [nupplaphil]
Enhanced handling of re-shares [annando]
Enhanced the ACL dialog [annando, MrPetovan]
Enhanced transmission of postings by email and email handling in general [annando]
Enhanced the updating process of contacts [annando]
Enhanced the import of RSS/Atom feeds [annando]
Enhanced the registration form for require approval setups [tobiasd]
Enhanced the follow process for the Diaspora* protocol [annando]
Enhanced the display of the saved searched [AlfredSK]
Enhanced the display of image titles [annando]
Enhanced the handling of OpenID [annando]
Enhanced the Vagrant devel VM [tobiasd]
Enhanced handling of HTML special entities [nathilia-peirce]
Enhanced hashes by using HMAC [nathilia-peirce]
Enhancements to the ActivityPub implementation [annando]
Fixed a problem with delivery of direct messages over ActivityPub [annando]
Fixed some problems with the remote auth functionality [annando]
Fixed an issue that prevented notifications for deleted postings be deleted themselves [annando]
Fixed a problem connecting to forums [annando]
Fixed messages in the admin panel [nupplaphil]
Fixed a problem when the log-file was not write-able [nupplaphil]
Fixed a problem with the caching directory for the password exposure check [MrPetovan]
Fixed a bug with detecting the browser language [nupplaphil]
Smart threading is now the default, but can be switched off by the user [annando]
Clarification: Posted order is now arrival order [annando]
Added router configuration file [nupplaphil]
Added drone.io as CI service [nupplaphil]
Added the ability to pin postings on account walls [annando]
Added various new API endpoints [annando, MrPetovan]
Added hooks for the email fetching process [annando]
Added support for nodeinfo 2 [annando]
Added export and import of followed contact data [tobiasd]
Added links to tag and category overview in the footer of postings [tobiasd]
Added config switch to use BCC instead of CC for ActivityPub delivery of non-public postings [annando]
Friendica Addons:
Update to the translations (CA, DE, ET) [translation teams]
buffer
marked as unsupported [annando]
Discourse
New addon to integrate Discourse discussions [annando]
gnot
UI improvements [tobiasd]
js_upload
The addon got rewritten to adopt the new ACL [MrPetovan]
mailstream:
Support for new img format was added [mexon]
BB Code is now included as plaintext [mexon]
@ -13,7 +61,12 @@ Version 2019.12-dev (unreleased)
ActivityPub "announce" notifications are not included [mexon]
Closed Issues:
1071, 7548, 7657, 7681
989, 1071, 1188, 1334, 2537, 3229, 3231, 3385, 4112, 4442, 4451,
5048, 5568, 5802, 6865, 7190, 7308, 7316, 7418, 7613, 7657, 7659,
7664, 7671, 7679, 7681, 7682, 7685, 7688, 7691, 7702, 7707, 7709,
7718, 7733, 7740, 7747, 7756, 7766, 7773, 7776, 7778, 7781, 7821,
7825, 7834, 7863, 7868, 7880, 7888, 7889, 7902, 7914, 7920, 7946,
7953, 7978
Version 2019.09 (2019-09-29)
Friendica Core:

View file

@ -1,5 +1,3 @@
23n
Abinoam P. Marques Jr.
Abraham Pérez Hernández
@ -47,6 +45,7 @@ bufalo1973
Calango Jr
Carlos Solís
Carsten Pfeiffer
Casper
Cat Gray
Chris Case
Christian González
@ -66,6 +65,7 @@ David Martín Miranda
David Rabel
Dean Townsley
Denis Chenu
dependabot[bot]
Devlon Duthie
Diego Souza
Domovoy
@ -115,6 +115,7 @@ Jens Tautenhahn
jensp
Jeroen De Meerleer
jeroenpraat
Joan Bar
JOduMonT
Johannes Schwab
John Brazil
@ -123,11 +124,13 @@ Jonny Tischbein
Josef Moravek
juanman
julia.domagalska
Julio Cova
Karel
Karolina
Keith Fernie
Klaus Weidenbach
Koyu Berteon
kPherox
Kris
Lea1995polish
Leberwurscht
@ -162,18 +165,20 @@ Michalina
Mike Macgirvin
miqrogroove
mytbk
nathilia-peirce
Nicola Spanti
Olaf Conradi
Oliver
Olivier
Olivier Mehani
Olivier Migeot
Ozero Dien
ozero dien
Paolo Wave
Pascal
Pascal Deklerck
Pavel Morozov
PerigGouanvic
peter
Peter Liebetrau
peturisfeld
Phigger Phigger
@ -193,6 +198,7 @@ Ralph
Ratten
rcmaniac
rebeka-catalina
René Wagner
repat
Ricardo Pereira
Rik 4

View file

@ -1 +1 @@
2019.12-dev
2019.12

View file

@ -32,7 +32,7 @@ use Friendica\Util\DateTimeFormat;
define('FRIENDICA_PLATFORM', 'Friendica');
define('FRIENDICA_CODENAME', 'Dalmatian Bellflower');
define('FRIENDICA_VERSION', '2019.12-dev');
define('FRIENDICA_VERSION', '2019.12');
define('DFRN_PROTOCOL_VERSION', '2.23');
define('NEW_UPDATE_ROUTINE_VERSION', 1170);

View file

@ -1,6 +1,6 @@
-- ------------------------------------------
-- Friendica 2019.12-dev (Dalmatian Bellflower)
-- DB_UPDATE_VERSION 1324
-- Friendica 2019.12-rc (Dalmatian Bellflower)
-- DB_UPDATE_VERSION 1326
-- ------------------------------------------
@ -66,6 +66,9 @@ CREATE TABLE IF NOT EXISTS `apcontact` (
`pubkey` text COMMENT '',
`baseurl` varchar(255) COMMENT 'baseurl of the ap contact',
`generator` varchar(255) COMMENT 'Name of the contact\'s system',
`following_count` int unsigned DEFAULT 0 COMMENT 'Number of following contacts',
`followers_count` int unsigned DEFAULT 0 COMMENT 'Number of followers',
`statuses_count` int unsigned DEFAULT 0 COMMENT 'Number of posts',
`updated` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '',
PRIMARY KEY(`url`),
INDEX `addr` (`addr`(32)),
@ -634,6 +637,7 @@ CREATE TABLE IF NOT EXISTS `item` (
INDEX `resource-id` (`resource-id`),
INDEX `deleted_changed` (`deleted`,`changed`),
INDEX `uid_wall_changed` (`uid`,`wall`,`changed`),
INDEX `mention_uid_id` (`mention`,`uid`,`id`),
INDEX `uid_eventid` (`uid`,`event-id`),
INDEX `icid` (`icid`),
INDEX `iaid` (`iaid`),

View file

@ -11,14 +11,25 @@ Authentication is the same as described in [Using the APIs](help/api#Authenticat
## Entities
These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/api/entities/).
These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/entities/).
## Implemented endpoints
- [GET /api/v1/follow_requests](https://docs.joinmastodon.org/api/rest/follow-requests/#get-api-v1-follow-requests)
- [`GET /api/v1/follow_requests`](https://docs.joinmastodon.org/methods/accounts/follow_requests#pending-follows)
- Returned IDs are specific to follow requests
- [`POST /api/v1/follow_requests/:id/authorize`](https://docs.joinmastodon.org/methods/accounts/follow_requests#accept-follow)
- `:id` is a follow request ID, not a regular account id
- [`POST /api/v1/follow_requests/:id/reject`](https://docs.joinmastodon.org/methods/accounts/follow_requests#reject-follow)
- `:id` is a follow request ID, not a regular account id
- `POST /api/v1/follow_requests/:id/ignore`
- Friendica-specific, hides the follow request from the list and prevents the remote contact from retrying.
- `:id` is a follow request ID, not a regular account id
- Returns a [Relationship](https://docs.joinmastodon.org/entities/relationship) object.
- [`GET /api/v1/instance`](https://docs.joinmastodon.org/methods/instance#fetch-instance)
- [`GET /api/v1/instance/peers`](https://docs.joinmastodon.org/methods/instance#list-of-connected-domains)
## Non-implemented endpoints
- [POST /api/v1/follow_requests/:id/authorize](https://docs.joinmastodon.org/api/rest/follow-requests/#post-api-v1-follow-requests-id-authorize)
- [POST /api/v1/follow_requests/:id/reject](https://docs.joinmastodon.org/api/rest/follow-requests/#post-api-v1-follow-requests-id-reject)
- [`GET /api/v1/instance/activity`](https://docs.joinmastodon.org/methods/instance#weekly-activity)

View file

@ -172,7 +172,7 @@ Here is a list of known working clients:
* Android
* [Friendiqa](https://git.friendi.ca/lubuwest/Friendiqa) (available in Google Playstore or from a binary repository you can add to [F-Droid](https://freunde.ma-nic.de/display/3e98eba8185a13c5bdbf3d1539646854))
* [Fedilab](https://gitlab.com/tom79/mastalab) (available in F-Droid and Google stores)
* [Fedilab](https://fedilab.app/) (available in F-Droid and Google stores)
* [DiCa](https://dica.mixi.cool/)
* AndStatus
* Twidere

View file

@ -55,7 +55,7 @@ Friendica Documentation and Resources
* [Move classes to `src`](help/Developer-How-To-Move-Classes-to-src)
* [Run tests](help/Tests)
* Reference
* [Twitter/GNU Social API Functions](help/api)
* [API endpoints](help/api)
* [Code (Doxygen generated - sets cookies)](doc/html/)
* [Protocol Documentation](help/Protocol)
* [Database schema documentation](help/database)

View file

@ -30,7 +30,7 @@ The account will expire after 7 days, but you can ask the server admin to keep y
### Requirements
* 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 *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
@ -63,8 +63,7 @@ If this is nothing for you, you might be interested in
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.
**OR**
@ -112,7 +111,8 @@ Please check the [troubleshooting](#Troubleshooting) section if running on MySQL
### Option A: Run the installer
Point your web browser to the new site and follow the instructions.
Before you point your web browser to the new site you need to copy `.htaccess-dist` to `.htaccess` for Apache installs.
Follow the instructions.
Please note any error messages and correct these before continuing.
If you need to specify a port for the connection to the database, you can do so in the host name setting for the database.
@ -328,7 +328,7 @@ If the database resides on the same machine, check that the database server name
### 500 Internal Error
This could be the result of one of our Apache directives not being supported by your version of Apache. Examine your apache server logs.
You might remove the line "Options -Indexes" from the .htaccess file if you are using a Windows server as this has been known to cause problems.
You might remove the line "Options -Indexes" from the `.htaccess` file if you are using a Windows server as this has been known to cause problems.
Also check your file permissions. Your website and all contents must generally be world-readable.
It is likely that your web server reported the source of the problem in its error log files.

View file

@ -107,6 +107,7 @@ Starte MySQL dann neu und es sollte klappen.
### Option A: Der manuelle Installer
Besuche deine Webseite mit deinem Browser und befolge die Anleitung.
Bevor du dies tust, kopiere die Datei `.htaccess-dist` nach `.htaccess`, wenn du den Apache Webserver verwendest.
Bitte beachte jeden Fehler und korrigiere diese, bevor du fortfährst.
Falls du einen Port für die Datenbankverbindung angeben musst, kannst du diesen in der Host-Eingabe Zeile angeben.

View file

@ -608,11 +608,6 @@ function api_get_user(App $a, $contact_id = null)
$contact = DBA::selectFirst('contact', [], ['uid' => 0, 'nurl' => Strings::normaliseLink($url)]);
if (DBA::isResult($contact)) {
// If no nick where given, extract it from the address
if (($contact['nick'] == "") || ($contact['name'] == $contact['nick'])) {
$contact['nick'] = api_get_nick($contact["url"]);
}
$ret = [
'id' => $contact["id"],
'id_str' => (string) $contact["id"],
@ -671,11 +666,6 @@ function api_get_user(App $a, $contact_id = null)
$countfollowers = 0;
$starred = 0;
// Add a nick if it isn't present there
if (($uinfo[0]['nick'] == "") || ($uinfo[0]['name'] == $uinfo[0]['nick'])) {
$uinfo[0]['nick'] = api_get_nick($uinfo[0]["url"]);
}
$pcontact_id = Contact::getIdForURL($uinfo[0]['url'], 0, true);
if (!empty($profile['about'])) {
@ -1419,32 +1409,37 @@ function api_users_search($type)
$userlist = [];
if (!empty($_GET['q'])) {
$r = q("SELECT id FROM `contact` WHERE `uid` = 0 AND `name` = '%s'", DBA::escape($_GET["q"]));
$contacts = Contact::selectToArray(
['id'],
[
'`uid` = 0 AND (`name` = ? OR `nick` = ? OR `url` = ? OR `addr` = ?)',
$_GET['q'],
$_GET['q'],
$_GET['q'],
$_GET['q'],
]
);
if (!DBA::isResult($r)) {
$r = q("SELECT `id` FROM `contact` WHERE `uid` = 0 AND `nick` = '%s'", DBA::escape($_GET["q"]));
}
if (DBA::isResult($r)) {
if (DBA::isResult($contacts)) {
$k = 0;
foreach ($r as $user) {
$user_info = api_get_user($a, $user["id"]);
foreach ($contacts as $contact) {
$user_info = api_get_user($a, $contact['id']);
if ($type == "xml") {
$userlist[$k++.":user"] = $user_info;
if ($type == 'xml') {
$userlist[$k++ . ':user'] = $user_info;
} else {
$userlist[] = $user_info;
}
}
$userlist = ["users" => $userlist];
$userlist = ['users' => $userlist];
} else {
throw new BadRequestException("User ".$_GET["q"]." not found.");
throw new NotFoundException('User ' . $_GET['q'] . ' not found.');
}
} else {
throw new BadRequestException("No user specified.");
throw new BadRequestException('No search term specified.');
}
return api_format_data("users", $type, $userlist);
return api_format_data('users', $type, $userlist);
}
/// @TODO move to top of file or somewhere better
@ -1505,7 +1500,9 @@ function api_search($type)
$a = \get_app();
$user_info = api_get_user($a);
if (api_user() === false || $user_info === false) { throw new ForbiddenException(); }
if (api_user() === false || $user_info === false) {
throw new ForbiddenException();
}
if (empty($_REQUEST['q'])) {
throw new BadRequestException('q parameter is required.');
@ -1569,7 +1566,21 @@ function api_search($type)
}
}
$statuses = Item::selectForUser(api_user(), [], $condition, $params);
$statuses = [];
if (parse_url($searchTerm, PHP_URL_SCHEME) != '') {
$id = Item::fetchByLink($searchTerm, api_user());
if (!$id) {
// Public post
$id = Item::fetchByLink($searchTerm);
}
if (!empty($id)) {
$statuses = Item::select([], ['id' => $id]);
}
}
$statuses = $statuses ?: Item::selectForUser(api_user(), [], $condition, $params);
$data['status'] = api_format_items(Item::inArray($statuses), $user_info);
@ -2147,8 +2158,8 @@ function api_statuses_mentions($type)
$start = max(0, ($page - 1) * $count);
$condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `item`.`id` > ? AND `author-id` != ?
AND `item`.`parent` IN (SELECT `iid` FROM `thread` WHERE `thread`.`uid` = ? AND `thread`.`mention` AND NOT `thread`.`ignored`)",
$condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `item`.`id` > ? AND `author-id` != ? AND `mention`
AND `item`.`parent` IN (SELECT `iid` FROM `thread` WHERE `thread`.`uid` = ? AND NOT `thread`.`ignored`)",
api_user(), GRAVITY_PARENT, GRAVITY_COMMENT, $since_id, $user_info['pid'], api_user()];
if ($max_id > 0) {
@ -2486,6 +2497,9 @@ function api_format_messages($item, $recipient, $sender)
function api_convert_item($item)
{
$body = $item['body'];
$entities = api_get_entitities($statustext, $body);
// Add pictures to the attachment array and remove them from the body
$attachments = api_get_attachments($body);
// Workaround for ostatus messages where the title is identically to the body
@ -2542,8 +2556,6 @@ function api_convert_item($item)
$statushtml .= BBCode::convert($item['plink']);
}
$entities = api_get_entitities($statustext, $body);
return [
"text" => $statustext,
"html" => $statushtml,
@ -2561,17 +2573,19 @@ function api_convert_item($item)
*/
function api_get_attachments(&$body)
{
$text = $body;
$text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $text);
$text = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $text);
$body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
$body = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $body);
$URLSearchString = "^\[\]";
$ret = preg_match_all("/\[img\]([$URLSearchString]*)\[\/img\]/ism", $text, $images);
if (!$ret) {
if (!preg_match_all("/\[img\]([$URLSearchString]*)\[\/img\]/ism", $body, $images)) {
return [];
}
// Remove all embedded pictures, since they are added as attachments
foreach ($images[0] as $orig) {
$body = str_replace($orig, '', $body);
}
$attachments = [];
foreach ($images[1] as $image) {
@ -2582,12 +2596,6 @@ function api_get_attachments(&$body)
}
}
if (strstr($_SERVER['HTTP_USER_AGENT'] ?? '', 'AndStatus')) {
foreach ($images[0] as $orig) {
$body = str_replace($orig, "", $body);
}
}
return $attachments;
}
@ -2630,7 +2638,6 @@ function api_get_entitities(&$text, $bbcode)
$bbcode = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '#$2', $bbcode);
$bbcode = preg_replace("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", '[url=$1]$2[/url]', $bbcode);
//$bbcode = preg_replace("/\[url\](.*?)\[\/url\]/ism",'[url=$1]$1[/url]',$bbcode);
$bbcode = preg_replace("/\[video\](.*?)\[\/video\]/ism", '[url=$1]$1[/url]', $bbcode);
$bbcode = preg_replace(
@ -2649,12 +2656,10 @@ function api_get_entitities(&$text, $bbcode)
$bbcode = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $bbcode);
//preg_match_all("/\[url\]([$URLSearchString]*)\[\/url\]/ism", $bbcode, $urls1);
preg_match_all("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $bbcode, $urls);
$ordered_urls = [];
foreach ($urls[1] as $id => $url) {
//$start = strpos($text, $url, $offset);
$start = iconv_strpos($text, $url, 0, "UTF-8");
if (!($start === false)) {
$ordered_urls[$start] = ["url" => $url, "title" => $urls[2][$id]];
@ -2664,7 +2669,7 @@ function api_get_entitities(&$text, $bbcode)
ksort($ordered_urls);
$offset = 0;
//foreach ($urls[1] AS $id=>$url) {
foreach ($ordered_urls as $url) {
if ((substr($url["title"], 0, 7) != "http://") && (substr($url["title"], 0, 8) != "https://")
&& !strpos($url["title"], "http://") && !strpos($url["title"], "https://")
@ -2679,7 +2684,6 @@ function api_get_entitities(&$text, $bbcode)
}
}
//$start = strpos($text, $url, $offset);
$start = iconv_strpos($text, $url["url"], $offset, "UTF-8");
if (!($start === false)) {
$entities["urls"][] = ["url" => $url["url"],
@ -2706,7 +2710,7 @@ function api_get_entitities(&$text, $bbcode)
$ordered_images[$start] = ['url' => $image, 'alt' => ''];
}
}
//$entities["media"] = array();
$offset = 0;
foreach ($ordered_images as $image) {
@ -2840,9 +2844,10 @@ function api_format_items_activities($item, $type = "json")
'attendyes' => [],
'attendno' => [],
'attendmaybe' => [],
'announce' => [],
];
$condition = ['uid' => $item['uid'], 'thr-parent' => $item['uri']];
$condition = ['uid' => $item['uid'], 'thr-parent' => $item['uri'], 'gravity' => GRAVITY_ACTIVITY];
$ret = Item::selectForUser($item['uid'], ['author-id', 'verb'], $condition);
while ($parent_item = Item::fetch($ret)) {
@ -2867,6 +2872,9 @@ function api_format_items_activities($item, $type = "json")
case Activity::ATTENDMAYBE:
$activities['attendmaybe'][] = $user;
break;
case Activity::ANNOUNCE:
$activities['announce'][] = $user;
break;
default:
break;
}
@ -3075,22 +3083,31 @@ function api_format_item($item, $type = "json", $status_user = null, $author_use
}
if (!empty($quoted_item)) {
$conv_quoted = api_convert_item($quoted_item);
$quoted_status = $status;
if ($quoted_item['id'] != $item['id']) {
$quoted_status = api_format_item($quoted_item);
/// @todo Only remove the attachments that are also contained in the quotes status
unset($status['attachments']);
unset($status['entities']);
} else {
$conv_quoted = api_convert_item($quoted_item);
$quoted_status = $status;
unset($quoted_status['attachments']);
unset($quoted_status['entities']);
unset($quoted_status['statusnet_conversation_id']);
$quoted_status['text'] = $conv_quoted['text'];
$quoted_status['statusnet_html'] = $conv_quoted['html'];
try {
$quoted_status["user"] = api_get_user($a, $quoted_item["author-id"]);
} catch (BadRequestException $e) {
// user not found. should be found?
/// @todo check if the user should be always found
$quoted_status["user"] = [];
}
}
unset($quoted_status['friendica_author']);
unset($quoted_status['friendica_owner']);
unset($quoted_status['friendica_activities']);
unset($quoted_status['friendica_private']);
unset($quoted_status['statusnet_conversation_id']);
$quoted_status['text'] = $conv_quoted['text'];
$quoted_status['statusnet_html'] = $conv_quoted['html'];
try {
$quoted_status["user"] = api_get_user($a, $quoted_item["author-id"]);
} catch (BadRequestException $e) {
// user not found. should be found?
/// @todo check if the user should be always found
$quoted_status["user"] = [];
}
}
if (!empty($retweeted_item)) {
@ -3606,6 +3623,7 @@ api_register_func('api/statusnet/version', 'api_statusnet_version', false);
*
* @param string $type Return type (atom, rss, xml, json)
*
* @param int $rel A contact relationship constant
* @return array|string|void
* @throws BadRequestException
* @throws ForbiddenException
@ -3614,7 +3632,7 @@ api_register_func('api/statusnet/version', 'api_statusnet_version', false);
* @throws UnauthorizedException
* @todo use api_format_data() to return data
*/
function api_ff_ids($type)
function api_ff_ids($type, int $rel)
{
if (!api_user()) {
throw new ForbiddenException();
@ -3626,26 +3644,29 @@ function api_ff_ids($type)
$stringify_ids = $_REQUEST['stringify_ids'] ?? false;
$r = q(
"SELECT `pcontact`.`id` FROM `contact`
INNER JOIN `contact` AS `pcontact` ON `contact`.`nurl` = `pcontact`.`nurl` AND `pcontact`.`uid` = 0
WHERE `contact`.`uid` = %s AND NOT `contact`.`self`",
intval(api_user())
$contacts = DBA::p("SELECT `pcontact`.`id`
FROM `contact`
INNER JOIN `contact` AS `pcontact`
ON `contact`.`nurl` = `pcontact`.`nurl`
AND `pcontact`.`uid` = 0
WHERE `contact`.`uid` = ?
AND NOT `contact`.`self`
AND `contact`.`rel` IN (?, ?)",
api_user(),
$rel,
Contact::FRIEND
);
if (!DBA::isResult($r)) {
return;
}
$ids = [];
foreach ($r as $rr) {
foreach (DBA::toArray($contacts) as $contact) {
if ($stringify_ids) {
$ids[] = $rr['id'];
$ids[] = $contact['id'];
} else {
$ids[] = intval($rr['id']);
$ids[] = intval($contact['id']);
}
}
return api_format_data("ids", $type, ['id' => $ids]);
return api_format_data('ids', $type, ['id' => $ids]);
}
/**
@ -3656,11 +3677,14 @@ function api_ff_ids($type)
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friends-ids
*/
function api_friends_ids($type)
{
return api_ff_ids($type);
return api_ff_ids($type, Contact::SHARING);
}
/**
@ -3671,11 +3695,14 @@ function api_friends_ids($type)
* @return array|string
* @throws BadRequestException
* @throws ForbiddenException
* @throws ImagickException
* @throws InternalServerErrorException
* @throws UnauthorizedException
* @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-ids
*/
function api_followers_ids($type)
{
return api_ff_ids($type);
return api_ff_ids($type, Contact::FOLLOWER);
}
/// @TODO move to top of file or somewhere better
@ -5064,14 +5091,17 @@ function api_friendica_remoteauth()
// traditional DFRN
$contact = DBA::selectFirst('contact', [], ['uid' => api_user(), 'nurl' => $c_url]);
if (!DBA::isResult($contact) || ($contact['network'] !== Protocol::DFRN)) {
if (!DBA::isResult($contact)) {
throw new BadRequestException("Unknown contact");
}
$cid = $contact['id'];
$dfrn_id = $contact['issued-id'] ?? $contact['dfrn-id'];
$dfrn_id = $contact['issued-id'] ?: $contact['dfrn-id'];
if (($contact['network'] !== Protocol::DFRN) || empty($dfrn_id)) {
System::externalRedirect($url ?: $c_url);
}
if ($contact['duplex'] && $contact['issued-id']) {
$orig_id = $contact['issued-id'];
@ -5184,94 +5214,25 @@ function api_share_as_retweet(&$item)
$reshared_item["created"] = $reshared['posted'];
$reshared_item["edited"] = $reshared['posted'];
// Try to fetch the original item
if (!empty($reshared['guid'])) {
$condition = ['guid' => $reshared['guid'], 'uid' => [0, $item['uid']]];
} elseif (!empty($reshared_item['plink']) && ($original_id = Item::searchByLink($reshared_item['plink']))) {
$condition = ['id' => $original_id];
} else {
$condition = [];
}
if (!empty($condition)) {
$original_item = Item::selectFirst([], $condition);
if (DBA::isResult($original_item)) {
$reshared_item = array_merge($reshared_item, $original_item);
}
}
return $reshared_item;
}
/**
*
* @param string $profile
*
* @return string|false
* @throws InternalServerErrorException
* @todo remove trailing junk from profile url
* @todo pump.io check has to check the website
*/
function api_get_nick($profile)
{
$nick = "";
$r = q(
"SELECT `nick` FROM `contact` WHERE `uid` = 0 AND `nurl` = '%s'",
DBA::escape(Strings::normaliseLink($profile))
);
if (DBA::isResult($r)) {
$nick = $r[0]["nick"];
}
if (!$nick == "") {
$r = q(
"SELECT `nick` FROM `contact` WHERE `uid` = 0 AND `nurl` = '%s'",
DBA::escape(Strings::normaliseLink($profile))
);
if (DBA::isResult($r)) {
$nick = $r[0]["nick"];
}
}
if (!$nick == "") {
$friendica = preg_replace("=https?://(.*)/profile/(.*)=ism", "$2", $profile);
if ($friendica != $profile) {
$nick = $friendica;
}
}
if (!$nick == "") {
$diaspora = preg_replace("=https?://(.*)/u/(.*)=ism", "$2", $profile);
if ($diaspora != $profile) {
$nick = $diaspora;
}
}
if (!$nick == "") {
$twitter = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $profile);
if ($twitter != $profile) {
$nick = $twitter;
}
}
if (!$nick == "") {
$StatusnetHost = preg_replace("=https?://(.*)/user/(.*)=ism", "$1", $profile);
if ($StatusnetHost != $profile) {
$StatusnetUser = preg_replace("=https?://(.*)/user/(.*)=ism", "$2", $profile);
if ($StatusnetUser != $profile) {
$UserData = Network::fetchUrl("http://".$StatusnetHost."/api/users/show.json?user_id=".$StatusnetUser);
$user = json_decode($UserData);
if ($user) {
$nick = $user->screen_name;
}
}
}
}
// To-Do: look at the page if its really a pumpio site
//if (!$nick == "") {
// $pumpio = preg_replace("=https?://(.*)/(.*)/=ism", "$2", $profile."/");
// if ($pumpio != $profile)
// $nick = $pumpio;
// <div class="media" id="profile-block" data-profile-id="acct:kabniel@microca.st">
//}
if ($nick != "") {
return $nick;
}
return false;
}
/**
*
* @param array $item
@ -5303,10 +5264,6 @@ function api_in_reply_to($item)
$parent = Item::selectFirst($fields, ['id' => $in_reply_to['status_id']]);
if (DBA::isResult($parent)) {
if ($parent['author-nick'] == "") {
$parent['author-nick'] = api_get_nick($parent['author-link']);
}
$in_reply_to['screen_name'] = (($parent['author-nick']) ? $parent['author-nick'] : $parent['author-name']);
$in_reply_to['user_id'] = intval($parent['author-id']);
$in_reply_to['user_id_str'] = (string) intval($parent['author-id']);

View file

@ -11,6 +11,7 @@ use Friendica\Core\Logger;
use Friendica\Core\Renderer;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Model\Item;
use Friendica\Model\User;
use Friendica\Protocol\Activity;
@ -775,17 +776,18 @@ function check_item_notification($itemid, $uid, $defaulttype = "") {
if ($item["parent-uri"] === $item["uri"]) {
// Send a notification for every new post?
// Either the contact had posted something directly
$send_notification = DBA::exists('contact', ['id' => $item['contact-id'], 'notify_new_posts' => true]);
// Or the contact is a mentioned forum
if (!$send_notification) {
$tags = q("SELECT `url` FROM `term` WHERE `otype` = %d AND `oid` = %d AND `type` = %d AND `uid` = %d",
intval(TERM_OBJ_POST), intval($itemid), intval(TERM_MENTION), intval($uid));
if (DBA::isResult($tags)) {
foreach ($tags AS $tag) {
$condition = ['nurl' => Strings::normaliseLink($tag["url"]), 'uid' => $uid, 'notify_new_posts' => true];
$r = DBA::exists('contact', $condition);
if ($r) {
$condition = ['nurl' => Strings::normaliseLink($tag["url"]), 'uid' => $uid, 'notify_new_posts' => true, 'contact-type' => Contact::TYPE_COMMUNITY];
if (DBA::exists('contact', $condition)) {
$send_notification = true;
}
}

View file

@ -136,7 +136,7 @@ function common_content(App $a)
$title = '';
$tab_str = '';
if ($cmd === 'loc' && $cid && local_user() == $uid) {
$tab_str = Module\Contact::getTabsHTML($a, $contact, 4);
$tab_str = Module\Contact::getTabsHTML($a, $contact, 5);
} else {
$title = L10n::t('Common Friends');
}

View file

@ -134,7 +134,7 @@ function crepair_content(App $a)
$update_profile = in_array($contact['network'], Protocol::FEDERATED);
$tab_str = Module\Contact::getTabsHTML($a, $contact, 5);
$tab_str = Module\Contact::getTabsHTML($a, $contact, 6);
$tpl = Renderer::getMarkupTemplate('crepair.tpl');
$o = Renderer::replaceMacros($tpl, [

View file

@ -93,7 +93,8 @@ function display_init(App $a)
}
if ($item["id"] != $item["parent"]) {
$item = Item::selectFirstForUser($item_user, $fields, ['id' => $item["parent"]]);
$parent = Item::selectFirstForUser($item_user, $fields, ['id' => $item["parent"]]);
$item = $parent ?: $item;
}
$profiledata = display_fetchauthor($a, $item);
@ -171,6 +172,8 @@ function display_content(App $a, $update = false, $update_uid = 0)
$o = '';
$item = null;
if ($update) {
$item_id = $_REQUEST['item_id'];
$item = Item::selectFirst(['uid', 'parent', 'parent-uri'], ['id' => $item_id]);
@ -220,7 +223,7 @@ function display_content(App $a, $update = false, $update_uid = 0)
}
}
if (!$item_id) {
if (empty($item)) {
throw new HTTPException\NotFoundException(L10n::t('The requested item doesn\'t exist or has been deleted.'));
}
@ -242,16 +245,20 @@ function display_content(App $a, $update = false, $update_uid = 0)
$is_remote_contact = false;
$item_uid = local_user();
if (isset($item_parent_uri)) {
$parent = null;
if (!empty($item_parent_uri)) {
$parent = Item::selectFirst(['uid'], ['uri' => $item_parent_uri, 'wall' => true]);
if (DBA::isResult($parent)) {
$a->profile['uid'] = ($a->profile['uid'] ?? 0) ?: $parent['uid'];
$a->profile['profile_uid'] = ($a->profile['profile_uid'] ?? 0) ?: $parent['uid'];
$is_remote_contact = Session::getRemoteContactID($a->profile['profile_uid']);
if ($is_remote_contact) {
$item_uid = $parent['uid'];
}
}
if (DBA::isResult($parent)) {
$a->profile['uid'] = ($a->profile['uid'] ?? 0) ?: $parent['uid'];
$a->profile['profile_uid'] = ($a->profile['profile_uid'] ?? 0) ?: $parent['uid'];
$is_remote_contact = Session::getRemoteContactID($a->profile['profile_uid']);
if ($is_remote_contact) {
$item_uid = $parent['uid'];
}
} else {
$a->profile = ['uid' => intval($item['uid']), 'profile_uid' => intval($item['uid'])];
}
$page_contact = DBA::selectFirst('contact', [], ['self' => true, 'uid' => $a->profile['uid']]);

View file

@ -14,7 +14,7 @@ use Friendica\Core\Renderer;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Module\Login;
use Friendica\Model\Contact;
use Friendica\Model\Introduction;
use Friendica\Model\Notify;
function notifications_post(App $a)
@ -30,43 +30,20 @@ function notifications_post(App $a)
}
if ($request_id) {
$intro = DBA::selectFirst('intro', ['id', 'contact-id', 'fid'], ['id' => $request_id, 'uid' => local_user()]);
/** @var Introduction $Intro */
$Intro = \Friendica\BaseObject::getClass(Introduction::class);
$Intro->fetch(['id' => $request_id, 'uid' => local_user()]);
if (DBA::isResult($intro)) {
$intro_id = $intro['id'];
$contact_id = $intro['contact-id'];
} else {
notice(L10n::t('Invalid request identifier.') . EOL);
return;
switch ($_POST['submit']) {
case L10n::t('Discard'):
$Intro->discard();
break;
case L10n::t('Ignore'):
$Intro->ignore();
break;
}
// If it is a friend suggestion, the contact is not a new friend but an existing friend
// that should not be deleted.
$fid = $intro['fid'];
if ($_POST['submit'] == L10n::t('Discard')) {
DBA::delete('intro', ['id' => $intro_id]);
if (!$fid) {
// When the contact entry had been created just for that intro, we want to get rid of it now
$condition = ['id' => $contact_id, 'uid' => local_user(),
'self' => false, 'pending' => true, 'rel' => [0, Contact::FOLLOWER]];
$contact_pending = DBA::exists('contact', $condition);
// Remove the "pending" to stop the reappearing in any case
DBA::update('contact', ['pending' => false], ['id' => $contact_id]);
if ($contact_pending) {
Contact::remove($contact_id);
}
}
$a->internalRedirect('notifications/intros');
}
if ($_POST['submit'] == L10n::t('Ignore')) {
DBA::update('intro', ['ignore' => true], ['id' => $intro_id]);
$a->internalRedirect('notifications/intros');
}
$a->internalRedirect('notifications/intros');
}
}

View file

@ -4,6 +4,7 @@ namespace Friendica\Api\Mastodon;
use Friendica\Content\Text\BBCode;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Util\DateTimeFormat;
/**
@ -55,31 +56,33 @@ class Account
/**
* Creates an account record from a contact record. Expects all contact table fields to be set
*
* @param array $contact
* @param array $contact Full contact table record
* @param array $apcontact Full apcontact table record
* @return Account
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function createFromContact(array $contact) {
public static function createFromContact(array $contact, array $apcontact = [])
{
$account = new Account();
$account->id = $contact['id'];
$account->username = $contact['nick'];
$account->acct = $contact['nick'];
$account->display_name = $contact['name'];
$account->locked = $contact['blocked'];
$account->created_at = DateTimeFormat::utc($contact['created'], DateTimeFormat::ATOM);
// No data is available from contact
$account->followers_count = 0;
$account->following_count = 0;
$account->statuses_count = 0;
$account->note = BBCode::convert($contact['about']);
$account->url = $contact['url'];
$account->avatar = $contact['avatar'];
$account->avatar_static = $contact['avatar'];
$account->id = $contact['id'];
$account->username = $contact['nick'];
$account->acct = $contact['nick'];
$account->display_name = $contact['name'];
$account->locked = !empty($apcontact['manually-approve']);
$account->created_at = DateTimeFormat::utc($contact['created'], DateTimeFormat::ATOM);
$account->followers_count = $apcontact['followers_count'] ?? 0;
$account->following_count = $apcontact['following_count'] ?? 0;
$account->statuses_count = $apcontact['statuses_count'] ?? 0;
$account->note = BBCode::convert($contact['about'], false);
$account->url = $contact['url'];
$account->avatar = $contact['avatar'];
$account->avatar_static = $contact['avatar'];
// No header picture in Friendica
$account->header = '';
$account->header_static = '';
$account->header = '';
$account->header_static = '';
// No custom emojis per account in Friendica
$account->emojis = [];
$account->emojis = [];
$account->bot = ($contact['contact-type'] == Contact::TYPE_NEWS);
return $account;
}

View file

@ -0,0 +1,86 @@
<?php
namespace Friendica\Api\Mastodon;
use Friendica\App;
use Friendica\Api\Mastodon\Account;
use Friendica\Api\Mastodon\Stats;
use Friendica\Core\Config;
use Friendica\Database\DBA;
use Friendica\Model\APContact;
use Friendica\Model\User;
use Friendica\Module\Register;
/**
* Class Instance
*
* @see https://docs.joinmastodon.org/api/entities/#instance
*/
class Instance
{
/** @var string (URL) */
var $uri;
/** @var string */
var $title;
/** @var string */
var $description;
/** @var string */
var $email;
/** @var string */
var $version;
/** @var array */
var $urls;
/** @var Stats */
var $stats;
/** @var string */
var $thumbnail;
/** @var array */
var $languages;
/** @var int */
var $max_toot_chars;
/** @var bool */
var $registrations;
/** @var bool */
var $approval_required;
/** @var Account|null */
var $contact_account;
/**
* Creates an instance record
*
* @param App $app
*
* @return Instance
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function get(App $app) {
$register_policy = intval(Config::get('config', 'register_policy'));
$instance = new Instance();
$instance->uri = $app->getBaseURL();
$instance->title = Config::get('config', 'sitename');
$instance->description = Config::get('config', 'info');
$instance->email = Config::get('config', 'admin_email');
$instance->version = FRIENDICA_VERSION;
$instance->urls = []; // Not supported
$instance->stats = Stats::get();
$instance->thumbnail = $app->getBaseURL() . (Config::get('system', 'shortcut_icon') ?? 'images/friendica-32.png');
$instance->languages = [Config::get('system', 'language')];
$instance->max_toot_chars = (int)Config::get('config', 'api_import_size', Config::get('config', 'max_import_size'));
$instance->registrations = ($register_policy != Register::CLOSED);
$instance->approval_required = ($register_policy == Register::APPROVE);
$instance->contact_account = [];
if (!empty(Config::get('config', 'admin_email'))) {
$adminList = explode(',', str_replace(' ', '', Config::get('config', 'admin_email')));
$administrator = User::getByEmail($adminList[0], ['nickname']);
if (!empty($administrator)) {
$adminContact = DBA::selectFirst('contact', [], ['nick' => $administrator['nickname'], 'self' => true]);
$apcontact = APContact::getByURL($adminContact['url'], false);
$instance->contact_account = Account::createFromContact($adminContact, $apcontact);
}
}
return $instance;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Friendica\Api\Mastodon;
use Friendica\Model\Contact;
use Friendica\Util\Network;
/**
* Class Relationship
*
* @see https://docs.joinmastodon.org/api/entities/#relationship
*/
class Relationship
{
/** @var int */
var $id;
/** @var bool */
var $following = false;
/** @var bool */
var $followed_by = false;
/** @var bool */
var $blocking = false;
/** @var bool */
var $muting = false;
/** @var bool */
var $muting_notifications = false;
/** @var bool */
var $requested = false;
/** @var bool */
var $domain_blocking = false;
/** @var bool */
var $showing_reblogs = false;
/** @var bool */
var $endorsed = false;
/**
* @param array $contact Full Contact table record
* @return Relationship
*/
public static function createFromContact(array $contact)
{
$relationship = new self();
$relationship->id = $contact['id'];
$relationship->following = in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND]);
$relationship->followed_by = in_array($contact['rel'], [Contact::FOLLOWER, Contact::FRIEND]);
$relationship->blocking = (bool)$contact['blocked'];
$relationship->muting = (bool)$contact['readonly'];
$relationship->muting_notifications = (bool)$contact['readonly'];
$relationship->requested = (bool)$contact['pending'];
$relationship->domain_blocking = Network::isUrlBlocked($contact['url']);
// Unsupported
$relationship->showing_reblogs = true;
// Unsupported
$relationship->endorsed = false;
return $relationship;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Friendica\Api\Mastodon;
use Friendica\Core\Config;
use Friendica\Core\Protocol;
use Friendica\Database\DBA;
/**
* Class Stats
*
* @see https://docs.joinmastodon.org/api/entities/#stats
*/
class Stats
{
/** @var int */
var $user_count;
/** @var int */
var $status_count;
/** @var int */
var $domain_count;
/**
* Creates a stats record
*
* @return Stats
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function get() {
$stats = new Stats();
if (!empty(Config::get('system', 'nodeinfo'))) {
$stats->user_count = intval(Config::get('nodeinfo', 'total_users'));
$stats->status_count = Config::get('nodeinfo', 'local_posts') + Config::get('nodeinfo', 'local_comments');
$stats->domain_count = DBA::count('gserver', ["`network` in (?, ?) AND `last_contact` >= `last_failure`", Protocol::DFRN, Protocol::ACTIVITYPUB]);
}
return $stats;
}
}

View file

@ -385,7 +385,7 @@ class App
* @deprecated 2019.09 - Use BaseURL->remove() instead
* @see BaseURL::remove()
*/
public function removeBaseURL($origURL)
public function removeBaseURL(string $origURL)
{
return $this->baseURL->remove($origURL);
}

View file

@ -251,10 +251,6 @@ class Module
call_user_func([$this->module_class, 'init'], $this->module_parameters);
// "rawContent" is especially meant for technical endpoints.
// This endpoint doesn't need any theme initialization or other comparable stuff.
call_user_func([$this->module_class, 'rawContent'], $this->module_parameters);
if ($server['REQUEST_METHOD'] === 'POST') {
Core\Hook::callAll($this->module . '_mod_post', $post);
call_user_func([$this->module_class, 'post'], $this->module_parameters);
@ -262,5 +258,9 @@ class Module
Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder);
call_user_func([$this->module_class, 'afterpost'], $this->module_parameters);
// "rawContent" is especially meant for technical endpoints.
// This endpoint doesn't need any theme initialization or other comparable stuff.
call_user_func([$this->module_class, 'rawContent'], $this->module_parameters);
}
}

95
src/BaseModel.php Normal file
View file

@ -0,0 +1,95 @@
<?php
namespace Friendica;
use Friendica\Database\Database;
use Friendica\Network\HTTPException;
use Psr\Log\LoggerInterface;
/**
* Class BaseModel
*
* The Model classes inheriting from this abstract class are meant to represent a single database record.
* The associated table name has to be provided in the child class, and the table is expected to have a unique `id` field.
*
* @property int id
*/
abstract class BaseModel
{
protected static $table_name;
/** @var Database */
protected $dba;
/** @var LoggerInterface */
protected $logger;
/**
* Model record abstraction.
* Child classes never have to interact directly with it.
* Please use the magic getter instead.
*
* @var array
*/
private $data = [];
public function __construct(Database $dba, LoggerInterface $logger)
{
$this->dba = $dba;
$this->logger = $logger;
}
/**
* Magic getter. This allows to retrieve model fields with the following syntax:
* - $model->field (outside of class)
* - $this->field (inside of class)
*
* @param $name
* @return mixed
* @throws HTTPException\InternalServerErrorException
*/
public function __get($name)
{
if (empty($this->data['id'])) {
throw new HTTPException\InternalServerErrorException(static::class . ' record uninitialized');
}
if (!array_key_exists($name, $this->data)) {
throw new HTTPException\InternalServerErrorException('Field ' . $name . ' not found in ' . static::class);
}
return $this->data[$name];
}
/**
* Fetches a single model record. The condition array is expected to contain a unique index (primary or otherwise).
*
* Chainable.
*
* @param array $condition
* @return BaseModel
* @throws HTTPException\NotFoundException
*/
public function fetch(array $condition)
{
$intro = $this->dba->selectFirst(static::$table_name, [], $condition);
if (!$intro) {
throw new HTTPException\NotFoundException(static::class . ' record not found.');
}
$this->data = $intro;
return $this;
}
/**
* Deletes the model record from the database.
* Prevents further methods from being called by wiping the internal model data.
*/
public function delete()
{
if ($this->dba->delete(static::$table_name, ['id' => $this->id])) {
$this->data = [];
}
}
}

View file

@ -354,6 +354,9 @@ class BBCode extends BaseObject
$post['url'] = $links[0][1];
}
// Simplify "video" element
$post['text'] = preg_replace('(\[video.*?\ssrc\s?=\s?([^\s\]]+).*?\].*?\[/video\])ism', '[video]$1[/video]', $post['text']);
// Now count the number of external media links
preg_match_all("(\[vimeo\](.*?)\[\/vimeo\])ism", $post['text'], $links1, PREG_SET_ORDER);
preg_match_all("(\[youtube\\](.*?)\[\/youtube\\])ism", $post['text'], $links2, PREG_SET_ORDER);
@ -395,15 +398,15 @@ class BBCode extends BaseObject
*/
public static function removeAttachment($body, $no_link_desc = false)
{
return preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism",
return preg_replace_callback("/\s*\[attachment (.*)\](.*?)\[\/attachment\]\s*/ism",
function ($match) use ($no_link_desc) {
$attach_data = self::getAttachmentData($match[0]);
if (empty($attach_data['url'])) {
return $match[0];
} elseif (empty($attach_data['title']) || $no_link_desc) {
return '[url]' . $attach_data['url'] . "[/url]\n";
return "\n[url]" . $attach_data['url'] . "[/url]\n";
} else {
return '[url=' . $attach_data['url'] . ']' . $attach_data['title'] . "[/url]\n";
return "\n[url=" . $attach_data['url'] . ']' . $attach_data['title'] . "[/url]\n";
}
}, $body);
}
@ -1504,8 +1507,29 @@ class BBCode extends BaseObject
$text = str_replace('[hr]', '<hr />', $text);
if (!$for_plaintext) {
$escaped = [];
// Escaping BBCodes susceptible to contain rogue URL we don'' want the autolinker to catch
$text = preg_replace_callback('#\[(url|img|audio|video|youtube|vimeo|share|attachment|iframe|bookmark).+?\[/\1\]#ism',
function ($matches) use (&$escaped) {
$return = '{escaped-' . count($escaped) . '}';
$escaped[] = $matches[0];
return $return;
},
$text
);
// Autolinker for isolated URLs
$text = preg_replace(Strings::autoLinkRegEx(), '[url]$1[/url]', $text);
// Restoring escaped blocks
$text = preg_replace_callback('/{escaped-([0-9]+)}/iU',
function ($matches) use ($escaped) {
return $escaped[intval($matches[1])] ?? $matches[0];
},
$text
);
}
// This is actually executed in Item::prepareBody()
@ -1606,6 +1630,9 @@ class BBCode extends BaseObject
$text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '<br/><img src="' .System::baseUrl() . '/images/lock_icon.gif" alt="' . L10n::t('Encrypted content') . '" title="' . '$1' . ' ' . L10n::t('Encrypted content') . '" /><br />', $text);
//$Text = preg_replace("/\[crypt=(.*?)\](.*?)\[\/crypt\]/ism", '<br/><img src="' .System::baseUrl() . '/images/lock_icon.gif" alt="' . L10n::t('Encrypted content') . '" title="' . '$1' . ' ' . L10n::t('Encrypted content') . '" /><br />', $Text);
// Simplify "video" element
$text = preg_replace('(\[video.*?\ssrc\s?=\s?([^\s\]]+).*?\].*?\[/video\])ism', '[video]$1[/video]', $text);
// Try to Oembed
if ($try_oembed) {
$text = preg_replace("/\[video\](.*?\.(ogg|ogv|oga|ogm|webm|mp4).*?)\[\/video\]/ism", '<video src="$1" controls="controls" width="' . $a->videowidth . '" height="' . $a->videoheight . '" loop="true"><a href="$1">$1</a></video>', $text);

View file

@ -335,6 +335,10 @@ class ACL extends BaseObject
*/
public static function getFullSelectorHTML(Page $page, array $user = null, bool $for_federation = false, array $default_permissions = [])
{
if (empty($user['uid'])) {
return '';
}
$page->registerFooterScript(Theme::getPathForFile('asset/typeahead.js/dist/typeahead.bundle.js'));
$page->registerFooterScript(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.js'));
$page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css'));

View file

@ -41,7 +41,7 @@ class System extends BaseObject
* @return string The cleaned url
* @throws \Exception
*/
public static function removedBaseUrl($orig_url)
public static function removedBaseUrl(string $orig_url)
{
return self::getApp()->removeBaseURL($orig_url);
}

View file

@ -334,9 +334,9 @@ class DBA extends BaseObject
/**
* @brief Delete a row from a table
*
* @param string $table Table name
* @param array $conditions Field condition(s)
* @param array $options
* @param string|array $table Table name
* @param array $conditions Field condition(s)
* @param array $options
* - cascade: If true we delete records in other tables that depend on the one we're deleting through
* relations (default: true)
*
@ -411,7 +411,7 @@ class DBA extends BaseObject
* @throws \Exception
* @see self::select
*/
public static function selectToArray(string $table, array $fields = [], array $condition = [], array $params = [])
public static function selectToArray($table, array $fields = [], array $condition = [], array $params = [])
{
return self::getClass(Database::class)->selectToArray($table, $fields, $condition, $params);
}

View file

@ -1377,10 +1377,10 @@ class Database
*
* @brief Retrieve a single record from a table
*
* @param string $table
* @param array $fields
* @param array $condition
* @param array $params
* @param string|array $table
* @param array $fields
* @param array $condition
* @param array $params
*
* @return bool|array
* @throws \Exception
@ -1412,7 +1412,7 @@ class Database
* @throws \Exception
* @see self::select
*/
public function selectToArray(string $table, array $fields = [], array $condition = [], array $params = [])
public function selectToArray($table, array $fields = [], array $condition = [], array $params = [])
{
return $this->toArray($this->select($table, $fields, $condition, $params));
}

View file

@ -84,7 +84,7 @@ class APContact extends BaseObject
public static function getByURL($url, $update = null)
{
if (empty($url)) {
return false;
return [];
}
$fetched_contact = false;
@ -110,7 +110,7 @@ class APContact extends BaseObject
}
if (!is_null($update)) {
return DBA::isResult($apcontact) ? $apcontact : false;
return DBA::isResult($apcontact) ? $apcontact : [];
}
if (DBA::isResult($apcontact)) {
@ -203,6 +203,33 @@ class APContact extends BaseObject
$apcontact['generator'] = JsonLD::fetchElement($compacted['as:generator'], 'as:name', '@value');
}
if (!empty($apcontact['following'])) {
$data = ActivityPub::fetchContent($apcontact['following']);
if (!empty($data)) {
if (!empty($data['totalItems'])) {
$apcontact['following_count'] = $data['totalItems'];
}
}
}
if (!empty($apcontact['followers'])) {
$data = ActivityPub::fetchContent($apcontact['followers']);
if (!empty($data)) {
if (!empty($data['totalItems'])) {
$apcontact['followers_count'] = $data['totalItems'];
}
}
}
if (!empty($apcontact['outbox'])) {
$data = ActivityPub::fetchContent($apcontact['outbox']);
if (!empty($data)) {
if (!empty($data['totalItems'])) {
$apcontact['statuses_count'] = $data['totalItems'];
}
}
}
// To-Do
// Unhandled

View file

@ -1230,7 +1230,7 @@ class Contact extends BaseObject
$follow_link = '';
$unfollow_link = '';
if (in_array($contact['network'], Protocol::NATIVE_SUPPORT)) {
if (!$contact['self'] && in_array($contact['network'], Protocol::NATIVE_SUPPORT)) {
if ($contact['uid'] && in_array($contact['rel'], [self::SHARING, self::FRIEND])) {
$unfollow_link = 'unfollow?url=' . urlencode($contact['url']);
} elseif(!$contact['pending']) {

View file

@ -876,7 +876,11 @@ class GContact
self::updateFromOutbox($outbox['first']['href'], $data);
return;
} elseif (!empty($outbox['first'])) {
self::updateFromOutbox($outbox['first'], $data);
if (is_string($outbox['first'])) {
self::updateFromOutbox($outbox['first'], $data);
} else {
Logger::warning('Unexpected data', ['outbox' => $outbox]);
}
return;
} else {
$items = [];

View file

@ -549,7 +549,7 @@ class GServer
$protocols[$protocol] = true;
}
if (!empty($protocols['friendica'])) {
if (!empty($protocols['dfrn'])) {
$server['network'] = Protocol::DFRN;
} elseif (!empty($protocols['activitypub'])) {
$server['network'] = Protocol::ACTIVITYPUB;

156
src/Model/Introduction.php Normal file
View file

@ -0,0 +1,156 @@
<?php
namespace Friendica\Model;
use Friendica\BaseModel;
use Friendica\Core\Protocol;
use Friendica\Network\HTTPException;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\Diaspora;
use Friendica\Util\DateTimeFormat;
/**
* @property int uid
* @property int fid
* @property int contact-id
* @property bool knowyou
* @property bool duplex
* @property string note
* @property string hash
* @property string datetime
* @property bool blocked
* @property bool ignored
*
* @package Friendica\Model
*/
final class Introduction extends BaseModel
{
static $table_name = 'intro';
/**
* Confirms a follow request and sends a notic to the remote contact.
*
* @param bool $duplex Is it a follow back?
* @param bool|null $hidden Should this contact be hidden? null = no change
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
* @throws HTTPException\NotFoundException
*/
public function confirm(bool $duplex = false, bool $hidden = null)
{
$this->logger->info('Confirming follower', ['cid' => $this->{'contact-id'}]);
$contact = Contact::selectFirst([], ['id' => $this->{'contact-id'}, 'uid' => $this->uid]);
if (!$contact) {
throw new HTTPException\NotFoundException('Contact record not found.');
}
$new_relation = $contact['rel'];
$writable = $contact['writable'];
if (!empty($contact['protocol'])) {
$protocol = $contact['protocol'];
} else {
$protocol = $contact['network'];
}
if ($protocol == Protocol::ACTIVITYPUB) {
ActivityPub\Transmitter::sendContactAccept($contact['url'], $contact['hub-verify'], $contact['uid']);
}
if (in_array($protocol, [Protocol::DIASPORA, Protocol::ACTIVITYPUB])) {
if ($duplex) {
$new_relation = Contact::FRIEND;
} else {
$new_relation = Contact::FOLLOWER;
}
if ($new_relation != Contact::FOLLOWER) {
$writable = 1;
}
}
$fields = [
'name-date' => DateTimeFormat::utcNow(),
'uri-date' => DateTimeFormat::utcNow(),
'blocked' => false,
'pending' => false,
'protocol' => $protocol,
'writable' => $writable,
'hidden' => $hidden ?? $contact['hidden'],
'rel' => $new_relation,
];
$this->dba->update('contact', $fields, ['id' => $contact['id']]);
array_merge($contact, $fields);
if ($new_relation == Contact::FRIEND) {
if ($protocol == Protocol::DIASPORA) {
$ret = Diaspora::sendShare(User::getById($contact['uid']), $contact);
$this->logger->info('share returns', ['return' => $ret]);
} elseif ($protocol == Protocol::ACTIVITYPUB) {
ActivityPub\Transmitter::sendActivity('Follow', $contact['url'], $contact['uid']);
}
}
$this->delete();
}
/**
* Silently ignores the introduction, hides it from notifications and prevents the remote contact from submitting
* additional follow requests.
*
* Chainable
*
* @return Introduction
* @throws \Exception
*/
public function ignore()
{
$this->dba->update('intro', ['ignore' => true], ['id' => $this->id]);
return $this;
}
/**
* Discards the introduction and sends a rejection message to AP contacts.
*
* @throws HTTPException\InternalServerErrorException
* @throws HTTPException\NotFoundException
* @throws \ImagickException
*/
public function discard()
{
// If it is a friend suggestion, the contact is not a new friend but an existing friend
// that should not be deleted.
if (!$this->fid) {
// When the contact entry had been created just for that intro, we want to get rid of it now
$condition = ['id' => $this->{'contact-id'}, 'uid' => $this->uid,
'self' => false, 'pending' => true, 'rel' => [0, Contact::FOLLOWER]];
if ($this->dba->exists('contact', $condition)) {
Contact::remove($this->{'contact-id'});
} else {
$this->dba->update('contact', ['pending' => false], ['id' => $this->{'contact-id'}]);
}
}
$contact = Contact::selectFirst([], ['id' => $this->{'contact-id'}, 'uid' => $this->uid]);
if (!$contact) {
throw new HTTPException\NotFoundException('Contact record not found.');
}
if (!empty($contact['protocol'])) {
$protocol = $contact['protocol'];
} else {
$protocol = $contact['network'];
}
if ($protocol == Protocol::ACTIVITYPUB) {
ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $contact['uid']);
}
$this->delete();
}
}

View file

@ -2621,7 +2621,7 @@ class Item extends BaseObject
"&num;$2", $item["body"]);
foreach ($tags as $tag) {
if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=') || $tag[1] == '#') {
if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=') || strlen($tag) < 2 || $tag[1] == '#') {
continue;
}

View file

@ -2,11 +2,13 @@
namespace Friendica\Module\Api\Mastodon;
use Friendica\Api\Mastodon\Account;
use Friendica\Api\Mastodon;
use Friendica\App\BaseURL;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Model\APContact;
use Friendica\Model\Contact;
use Friendica\Model\Introduction;
use Friendica\Module\Base\Api;
use Friendica\Network\HTTPException;
@ -19,7 +21,40 @@ class FollowRequests extends Api
{
parent::init($parameters);
self::login();
if (!self::login()) {
throw new HTTPException\UnauthorizedException();
}
}
public static function post(array $parameters = [])
{
parent::post($parameters);
/** @var Introduction $Intro */
$Intro = self::getClass(Introduction::class);
$Intro->fetch(['id' => $parameters['id'], 'uid' => self::$current_user_id]);
$contactId = $Intro->{'contact-id'};
$relationship = new Mastodon\Relationship();
$relationship->id = $contactId;
switch ($parameters['action']) {
case 'authorize':
$Intro->confirm();
$relationship = Mastodon\Relationship::createFromContact(Contact::getById($contactId));
break;
case 'ignore':
$Intro->ignore();
break;
case 'reject':
$Intro->discard();
break;
default:
throw new HTTPException\BadRequestException('Unexpected action parameter, expecting "authorize", "ignore" or "reject"');
}
System::jsonExit($relationship);
}
/**
@ -34,26 +69,32 @@ class FollowRequests extends Api
$limit = intval($_GET['limit'] ?? 40);
if (isset($since_id) && isset($max_id)) {
$condition = ['`uid` = ? AND NOT `self` AND `pending` AND `id` > ? AND `id` < ?', self::$current_user_id, $since_id, $max_id];
$condition = ['`uid` = ? AND NOT `ignore` AND `id` > ? AND `id` < ?', self::$current_user_id, $since_id, $max_id];
} elseif (isset($since_id)) {
$condition = ['`uid` = ? AND NOT `self` AND `pending` AND `id` > ?', self::$current_user_id, $since_id];
$condition = ['`uid` = ? AND NOT `ignore` AND `id` > ?', self::$current_user_id, $since_id];
} elseif (isset($max_id)) {
$condition = ['`uid` = ? AND NOT `self` AND `pending` AND `id` < ?', self::$current_user_id, $max_id];
$condition = ['`uid` = ? AND NOT `ignore` AND `id` < ?', self::$current_user_id, $max_id];
} else {
$condition = ['`uid` = ? AND NOT `self` AND `pending`', self::$current_user_id];
$condition = ['`uid` = ? AND NOT `ignore`', self::$current_user_id];
}
$count = DBA::count('contact', $condition);
$count = DBA::count('intro', $condition);
$contacts = Contact::selectToArray(
$intros = DBA::selectToArray(
'intro',
[],
$condition,
['order' => ['id' => 'DESC'], 'limit' => $limit]
);
$return = [];
foreach ($contacts as $contact) {
$account = Account::createFromContact($contact);
foreach ($intros as $intro) {
$contact = Contact::getById($intro['contact-id']);
$apcontact = APContact::getByURL($contact['url'], false);
$account = Mastodon\Account::createFromContact($contact, $apcontact);
// Not ideal, the same "account" can have multiple ids depending on the context
$account->id = $intro['id'];
$return[] = $account;
}
@ -68,9 +109,9 @@ class FollowRequests extends Api
$links = [];
if ($count > $limit) {
$links[] = '<' . $BaseURL->get() . '/api/v1/follow_requests?' . http_build_query($base_query + ['max_id' => $contacts[count($contacts) - 1]['id']]) . '>; rel="next"';
$links[] = '<' . $BaseURL->get() . '/api/v1/follow_requests?' . http_build_query($base_query + ['max_id' => $intros[count($intros) - 1]['id']]) . '>; rel="next"';
}
$links[] = '<' . $BaseURL->get() . '/api/v1/follow_requests?' . http_build_query($base_query + ['since_id' => $contacts[0]['id']]) . '>; rel="prev"';
$links[] = '<' . $BaseURL->get() . '/api/v1/follow_requests?' . http_build_query($base_query + ['since_id' => $intros[0]['id']]) . '>; rel="prev"';
header('Link: ' . implode(', ', $links));

View file

@ -0,0 +1,22 @@
<?php
namespace Friendica\Module\Api\Mastodon;
use Friendica\Api\Mastodon\Instance as InstanceEntity;
use Friendica\Core\System;
use Friendica\Module\Base\Api;
/**
* @see https://docs.joinmastodon.org/api/rest/instances/
*/
class Instance extends Api
{
/**
* @param array $parameters
* @throws HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
System::jsonExit(InstanceEntity::get(self::getApp()));
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Friendica\Module\Api\Mastodon\Instance;
use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Module\Base\Api;
use Friendica\Network\HTTPException;
use Friendica\Util\Network;
/**
* Undocumented API endpoint that is implemented by both Mastodon and Pleroma
*/
class Peers extends Api
{
/**
* @param array $parameters
* @throws HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
$return = [];
// We only select for Friendica and ActivityPub servers, since it is expected to only deliver AP compatible systems here.
$instances = DBA::select('gserver', ['url'], ["`network` in (?, ?) AND `last_contact` >= `last_failure`", Protocol::DFRN, Protocol::ACTIVITYPUB]);
while ($instance = DBA::fetch($instances)) {
$urldata = parse_url($instance['url']);
unset($urldata['scheme']);
$return[] = ltrim(Network::unparseURL($urldata), '/');
}
DBA::close($instances);
System::jsonExit($return);
}
}

View file

@ -54,6 +54,7 @@ class Api extends BaseModule
*
* @brief Login API user
*
* @return bool Was a user authenticated?
* @throws HTTPException\ForbiddenException
* @throws HTTPException\UnauthorizedException
* @throws HTTPException\InternalServerErrorException
@ -69,6 +70,8 @@ class Api extends BaseModule
api_login(self::getApp());
self::$current_user_id = api_user();
return (bool)self::$current_user_id;
}
/**

View file

@ -646,21 +646,25 @@ class Contact extends BaseModule
return $arr['output'];
}
$select_uid = local_user();
// @TODO: Replace with parameter from router
$type = $a->argv[1] ?? '';
switch ($type) {
case 'blocked':
$sql_extra = " AND `blocked`";
$sql_extra = sprintf(" AND EXISTS(SELECT `id` from `user-contact` WHERE `contact`.`id` = `user-contact`.`cid` and `user-contact`.`uid` = %d and `user-contact`.`blocked`)", intval(local_user()));
$select_uid = 0;
break;
case 'hidden':
$sql_extra = " AND `hidden` AND NOT `blocked`";
$sql_extra = " AND `hidden` AND NOT `blocked` AND NOT `pending`";
break;
case 'ignored':
$sql_extra = " AND `readonly` AND NOT `blocked`";
$sql_extra = sprintf(" AND EXISTS(SELECT `id` from `user-contact` WHERE `contact`.`id` = `user-contact`.`cid` and `user-contact`.`uid` = %d and `user-contact`.`ignored`)", intval(local_user()));
$select_uid = 0;
break;
case 'archived':
$sql_extra = " AND `archive` AND NOT `blocked`";
$sql_extra = " AND `archive` AND NOT `blocked` AND NOT `pending`";
break;
case 'pending':
$sql_extra = sprintf(" AND `pending` AND NOT `archive` AND ((`rel` = %d)
@ -762,21 +766,21 @@ class Contact extends BaseModule
$sql_extra2 = ((($sort_type > 0) && ($sort_type <= Model\Contact::FRIEND)) ? sprintf(" AND `rel` = %d ", intval($sort_type)) : '');
$sql_extra3 = Widget::unavailableNetworks();
$r = q("SELECT COUNT(*) AS `total` FROM `contact`
WHERE `uid` = %d AND `self` = 0 $sql_extra $sql_extra2 ",
intval($_SESSION['uid'])
WHERE `uid` = %d AND `self` = 0 $sql_extra $sql_extra2 $sql_extra3",
intval($select_uid)
);
if (DBA::isResult($r)) {
$total = $r[0]['total'];
}
$pager = new Pager($a->query_string);
$sql_extra3 = Widget::unavailableNetworks();
$contacts = [];
$r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` = 0 $sql_extra $sql_extra2 $sql_extra3 ORDER BY `name` ASC LIMIT %d , %d ",
intval($_SESSION['uid']),
intval($select_uid),
$pager->getStart(),
$pager->getItemsPerPage()
);

View file

@ -1,17 +1,9 @@
<?php
namespace Friendica\Module;
use Friendica\App;
use Friendica\BaseModule;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Model\User;
use Friendica\Protocol\Diaspora;
use Friendica\Protocol\ActivityPub;
use Friendica\Util\DateTimeFormat;
use Friendica\Model\Introduction;
/**
* Process follow request confirmations
@ -30,67 +22,15 @@ class FollowConfirm extends BaseModule
$intro_id = intval($_POST['intro_id'] ?? 0);
$duplex = intval($_POST['duplex'] ?? 0);
$cid = intval($_POST['contact_id'] ?? 0);
$hidden = intval($_POST['hidden'] ?? 0);
if (empty($cid)) {
notice(L10n::t('No given contact.') . EOL);
return;
}
/** @var Introduction $Intro */
$Intro = self::getClass(Introduction::class);
$Intro->fetch(['id' => $intro_id, 'uid' => local_user()]);
Logger::info('Confirming follower', ['cid' => $cid]);
$cid = $Intro->{'contact-id'};
$contact = DBA::selectFirst('contact', [], ['id' => $cid, 'uid' => $uid]);
if (!DBA::isResult($contact)) {
Logger::warning('Contact not found in DB.', ['cid' => $cid]);
notice(L10n::t('Contact not found.') . EOL);
return;
}
$relation = $contact['rel'];
$new_relation = $contact['rel'];
$writable = $contact['writable'];
if (!empty($contact['protocol'])) {
$protocol = $contact['protocol'];
} else {
$protocol = $contact['network'];
}
if ($protocol == Protocol::ACTIVITYPUB) {
ActivityPub\Transmitter::sendContactAccept($contact['url'], $contact['hub-verify'], $uid);
}
if (in_array($protocol, [Protocol::DIASPORA, Protocol::ACTIVITYPUB])) {
if ($duplex) {
$new_relation = Contact::FRIEND;
} else {
$new_relation = Contact::FOLLOWER;
}
if ($new_relation != Contact::FOLLOWER) {
$writable = 1;
}
}
$fields = ['name-date' => DateTimeFormat::utcNow(),
'uri-date' => DateTimeFormat::utcNow(),
'blocked' => false, 'pending' => false, 'protocol' => $protocol,
'writable' => $writable, 'hidden' => $hidden, 'rel' => $new_relation];
DBA::update('contact', $fields, ['id' => $cid]);
if ($new_relation == Contact::FRIEND) {
if ($protocol == Protocol::DIASPORA) {
$user = User::getById($uid);
$contact = Contact::getById($cid);
$ret = Diaspora::sendShare($user, $contact);
Logger::info('share returns', ['return' => $ret]);
} elseif ($protocol == Protocol::ACTIVITYPUB) {
ActivityPub\Transmitter::sendActivity('Follow', $contact['url'], $uid);
}
}
DBA::delete('intro', ['id' => $intro_id]);
$Intro->confirm($duplex, $hidden);
$a->internalRedirect('contact/' . intval($cid));
}

View file

@ -85,15 +85,20 @@ class Compose extends BaseModule
$type = 'post';
$doesFederate = true;
if ($_REQUEST['contact_allow']
. $_REQUEST['group_allow']
. $_REQUEST['contact_deny']
. $_REQUEST['group_deny'])
$contact_allow = $_REQUEST['contact_allow'] ?? '';
$group_allow = $_REQUEST['group_allow'] ?? '';
$contact_deny = $_REQUEST['contact_deny'] ?? '';
$group_deny = $_REQUEST['group_deny'] ?? '';
if ($contact_allow
. $group_allow
. $contact_deny
. $group_deny)
{
$contact_allow_list = $_REQUEST['contact_allow'] ? explode(',', $_REQUEST['contact_allow']) : [];
$group_allow_list = $_REQUEST['group_allow'] ? explode(',', $_REQUEST['group_allow']) : [];
$contact_deny_list = $_REQUEST['contact_deny'] ? explode(',', $_REQUEST['contact_deny']) : [];
$group_deny_list = $_REQUEST['group_deny'] ? explode(',', $_REQUEST['group_deny']) : [];
$contact_allow_list = $contact_allow ? explode(',', $contact_allow) : [];
$group_allow_list = $group_allow ? explode(',', $group_allow) : [];
$contact_deny_list = $contact_deny ? explode(',', $contact_deny) : [];
$group_deny_list = $group_deny ? explode(',', $group_deny) : [];
}
break;

View file

@ -126,7 +126,7 @@ class NodeInfo extends BaseModule
$nodeinfo = [
'version' => '1.0',
'software' => [
'name' => 'friendica',
'name' => 'Friendica',
'version' => FRIENDICA_VERSION . '-' . DB_UPDATE_VERSION,
],
'protocols' => [
@ -191,7 +191,7 @@ class NodeInfo extends BaseModule
$nodeinfo = [
'version' => '2.0',
'software' => [
'name' => 'friendica',
'name' => 'Friendica',
'version' => FRIENDICA_VERSION . '-' . DB_UPDATE_VERSION,
],
'protocols' => ['dfrn', 'activitypub'],

View file

@ -67,7 +67,7 @@ class Verify extends BaseModule
'$errors_label' => L10n::tt('Error', 'Errors', count(self::$errors)),
'$errors' => self::$errors,
'$recovery_message' => L10n::t('Dont have your phone? <a href="%s">Enter a two-factor recovery code</a>', '2fa/recovery'),
'$verify_code' => ['verify_code', L10n::t('Please enter a code from your authentication app'), '', '', 'required', 'autofocus placeholder="000000"', 'number'],
'$verify_code' => ['verify_code', L10n::t('Please enter a code from your authentication app'), '', '', 'required', 'autofocus placeholder="000000"', 'tel'],
'$verify_label' => L10n::t('Verify code and complete login'),
]);
}

View file

@ -1574,7 +1574,7 @@ class Probe
$attr[$attribute->name] = trim($attribute->value);
}
if ($feed_url == "") {
if (empty($feed_url) && !empty($attr['href'])) {
$feed_url = $attr["href"];
}
}

View file

@ -252,7 +252,7 @@ class Delivery extends BaseObject
private static function deliverDFRN($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup)
{
// Transmit Diaspora reshares via Diaspora if the Friendica contact support Diaspora
if (Diaspora::isReshare($target_item['body']) && !empty(Diaspora::personByHandle(contact['addr'], false))) {
if (Diaspora::isReshare($target_item['body']) && !empty(Diaspora::personByHandle($contact['addr'], false))) {
Logger::info('Reshare will be transmitted via Diaspora', ['url' => $contact['url'], 'guid' => ($target_item['guid'] ?? '') ?: $target_item['id']]);
self::deliverDiaspora($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup);
return;

View file

@ -562,7 +562,7 @@ class OnePoll
}
// Decoding the header
$subject = imap_mime_header_decode($meta->subject);
$subject = imap_mime_header_decode($meta->subject ?? '');
$datarray['title'] = "";
foreach ($subject as $subpart) {
if ($subpart->charset != "default") {

View file

@ -34,7 +34,7 @@
use Friendica\Database\DBA;
if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1324);
define('DB_UPDATE_VERSION', 1327);
}
return [
@ -102,6 +102,9 @@ return [
"pubkey" => ["type" => "text", "comment" => ""],
"baseurl" => ["type" => "varchar(255)", "comment" => "baseurl of the ap contact"],
"generator" => ["type" => "varchar(255)", "comment" => "Name of the contact's system"],
"following_count" => ["type" => "int unsigned", "default" => 0, "comment" => "Number of following contacts"],
"followers_count" => ["type" => "int unsigned", "default" => 0, "comment" => "Number of followers"],
"statuses_count" => ["type" => "int unsigned", "default" => 0, "comment" => "Number of posts"],
"updated" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => ""]
],
"indexes" => [
@ -702,6 +705,7 @@ return [
"resource-id" => ["resource-id"],
"deleted_changed" => ["deleted", "changed"],
"uid_wall_changed" => ["uid", "wall", "changed"],
"mention_uid_id" => ["mention", "uid", "id"],
"uid_eventid" => ["uid", "event-id"],
"icid" => ["icid"],
"iaid" => ["iaid"],

View file

@ -30,6 +30,9 @@ return [
'/api' => [
'/v1' => [
'/follow_requests' => [Module\Api\Mastodon\FollowRequests::class, [R::GET ]],
'/follow_requests/{id:\d+}/{action}' => [Module\Api\Mastodon\FollowRequests::class, [ R::POST]],
'/instance' => [Module\Api\Mastodon\Instance::class, [R::GET]],
'/instance/peers' => [Module\Api\Mastodon\Instance\Peers::class, [R::GET]],
],
],

View file

@ -15,6 +15,7 @@ use Friendica\Core\Session;
use Friendica\Core\Session\ISession;
use Friendica\Core\System;
use Friendica\Database\Database;
use Friendica\Model\Contact;
use Friendica\Network\HTTPException;
use Friendica\Test\Util\Database\StaticDatabase;
use Monolog\Handler\TestHandler;
@ -2929,8 +2930,8 @@ class ApiTest extends DatabaseTest
*/
public function testApiFfIds()
{
$result = api_ff_ids('json');
$this->assertNull($result);
$result = api_ff_ids('json', Contact::FOLLOWER);
$this->assertEquals(['id' => []], $result);
}
/**
@ -2952,7 +2953,7 @@ class ApiTest extends DatabaseTest
public function testApiFfIdsWithoutAuthenticatedUser()
{
$_SESSION['authenticated'] = false;
api_ff_ids('json');
api_ff_ids('json', Contact::FOLLOWER);
}
/**
@ -2963,7 +2964,7 @@ class ApiTest extends DatabaseTest
public function testApiFriendsIds()
{
$result = api_friends_ids('json');
$this->assertNull($result);
$this->assertEquals(['id' => []], $result);
}
/**
@ -2974,7 +2975,7 @@ class ApiTest extends DatabaseTest
public function testApiFollowersIds()
{
$result = api_followers_ids('json');
$this->assertNull($result);
$this->assertEquals(['id' => []], $result);
}
/**
@ -3723,28 +3724,6 @@ class ApiTest extends DatabaseTest
$this->markTestIncomplete();
}
/**
* Test the api_get_nick() function.
*
* @return void
*/
public function testApiGetNick()
{
$result = api_get_nick($this->otherUser['nurl']);
$this->assertEquals('othercontact', $result);
}
/**
* Test the api_get_nick() function with a wrong URL.
*
* @return void
*/
public function testApiGetNickWithWrongUrl()
{
$result = api_get_nick('wrong_url');
$this->assertFalse($result);
}
/**
* Test the api_in_reply_to() function.
*

View file

@ -13,13 +13,13 @@
<whitelist>
<directory suffix=".php">..</directory>
<exclude>
<directory suffix=".php">config/</directory>
<directory suffix=".php">doc/</directory>
<directory suffix=".php">images/</directory>
<directory suffix=".php">library/</directory>
<directory suffix=".php">spec/</directory>
<directory suffix=".php">tests/</directory>
<directory suffix=".php">view/</directory>
<directory suffix=".php">../config</directory>
<directory suffix=".php">../doc</directory>
<directory suffix=".php">../images</directory>
<directory suffix=".php">../library</directory>
<directory suffix=".php">../spec</directory>
<directory suffix=".php">../tests</directory>
<directory suffix=".php">../view</directory>
</exclude>
</whitelist>
</filter>

View file

@ -396,3 +396,15 @@ function update_1323()
return Update::SUCCESS;
}
function update_1327()
{
$contacts = DBA::select('contact', ['uid', 'id', 'blocked', 'readonly'], ["`uid` != ? AND (`blocked` OR `readonly`) AND NOT `pending`", 0]);
while ($contact = DBA::fetch($contacts)) {
Contact::setBlockedForUser($contact['id'], $contact['uid'], $contact['blocked']);
Contact::setIgnoredForUser($contact['id'], $contact['uid'], $contact['readonly']);
}
DBA::close($contacts);
return Update::SUCCESS;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -145,6 +145,15 @@
identify: function(obj) { return obj.type + '-' + obj.id.toString(); },
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(['name', 'addr']),
queryTokenizer: Bloodhound.tokenizers.whitespace,
sorter: function (itemA, itemB) {
if (itemA.name === itemB.name) {
return 0;
} else if (itemA.name > itemB.name) {
return 1;
} else {
return -1;
}
},
});
acl.initialize();