Merge branch 'master' 2019.12 into develop

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

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") {