Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2793 lines
87 KiB

<?php
/**
* @copyright Copyright (C) 2020, Friendica
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Model;
use Friendica\App\BaseURL;
use Friendica\Content\Pager;
use Friendica\Core\Hook;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Session;
3 years ago
use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Notify\Type;
use Friendica\Network\HTTPException;
use Friendica\Network\Probe;
use Friendica\Protocol\Activity;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\DFRN;
use Friendica\Protocol\Diaspora;
use Friendica\Protocol\OStatus;
use Friendica\Protocol\Salmon;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Images;
use Friendica\Util\Network;
use Friendica\Util\Strings;
/**
* functions for interacting with a contact
*/
class Contact
{
/**
* @deprecated since version 2019.03
* @see User::PAGE_FLAGS_NORMAL
*/
const PAGE_NORMAL = User::PAGE_FLAGS_NORMAL;
/**
* @deprecated since version 2019.03
* @see User::PAGE_FLAGS_SOAPBOX
*/
const PAGE_SOAPBOX = User::PAGE_FLAGS_SOAPBOX;
/**
* @deprecated since version 2019.03
* @see User::PAGE_FLAGS_COMMUNITY
*/
const PAGE_COMMUNITY = User::PAGE_FLAGS_COMMUNITY;
/**
* @deprecated since version 2019.03
* @see User::PAGE_FLAGS_FREELOVE
*/
const PAGE_FREELOVE = User::PAGE_FLAGS_FREELOVE;
/**
* @deprecated since version 2019.03
* @see User::PAGE_FLAGS_BLOG
*/
const PAGE_BLOG = User::PAGE_FLAGS_BLOG;
/**
* @deprecated since version 2019.03
* @see User::PAGE_FLAGS_PRVGROUP
*/
const PAGE_PRVGROUP = User::PAGE_FLAGS_PRVGROUP;
/**
* @}
*/
/**
* Account types
*
* TYPE_UNKNOWN - the account has been imported from gcontact where this is the default type value
*
* TYPE_PERSON - the account belongs to a person
* Associated page types: PAGE_NORMAL, PAGE_SOAPBOX, PAGE_FREELOVE
*
* TYPE_ORGANISATION - the account belongs to an organisation
* Associated page type: PAGE_SOAPBOX
*
* TYPE_NEWS - the account is a news reflector
* Associated page type: PAGE_SOAPBOX
*
* TYPE_COMMUNITY - the account is community forum
* Associated page types: PAGE_COMMUNITY, PAGE_PRVGROUP
*
* TYPE_RELAY - the account is a relay
* This will only be assigned to contacts, not to user accounts
* @{
*/
const TYPE_UNKNOWN = -1;
const TYPE_PERSON = User::ACCOUNT_TYPE_PERSON;
const TYPE_ORGANISATION = User::ACCOUNT_TYPE_ORGANISATION;
const TYPE_NEWS = User::ACCOUNT_TYPE_NEWS;
const TYPE_COMMUNITY = User::ACCOUNT_TYPE_COMMUNITY;
const TYPE_RELAY = User::ACCOUNT_TYPE_RELAY;
/**
* @}
*/
/**
* Contact_is
*
* Relationship types
* @{
*/
const FOLLOWER = 1;
const SHARING = 2;
const FRIEND = 3;
/**
* @}
*/
/**
* @param array $fields Array of selected fields, empty for all
* @param array $condition Array of fields for condition
* @param array $params Array of several parameters
* @return array
* @throws \Exception
*/
public static function selectToArray(array $fields = [], array $condition = [], array $params = [])
{
return DBA::selectToArray('contact', $fields, $condition, $params);
}
/**
* @param array $fields Array of selected fields, empty for all
* @param array $condition Array of fields for condition
* @param array $params Array of several parameters
* @return array
* @throws \Exception
*/
public static function selectFirst(array $fields = [], array $condition = [], array $params = [])
{
$contact = DBA::selectFirst('contact', $fields, $condition, $params);
return $contact;
}
/**
* Insert a row into the contact table
* Important: You can't use DBA::lastInsertId() after this call since it will be set to 0.
*
* @param array $fields field array
* @param bool $on_duplicate_update Do an update on a duplicate entry
*
* @return boolean was the insert successful?
* @throws \Exception
*/
public static function insert(array $fields, bool $on_duplicate_update = false)
{
$ret = DBA::insert('contact', $fields, $on_duplicate_update);
$contact = DBA::selectFirst('contact', ['nurl', 'uid'], ['id' => DBA::lastInsertId()]);
if (!DBA::isResult($contact)) {
// Shouldn't happen
return $ret;
}
// Search for duplicated contacts and get rid of them
3 years ago
self::removeDuplicates($contact['nurl'], $contact['uid']);
return $ret;
}
/**
3 years ago
* @param integer $id Contact ID
* @param array $fields Array of selected fields, empty for all
* @return array|boolean Contact record if it exists, false otherwise
* @throws \Exception
*/
3 years ago
public static function getById($id, $fields = [])
{
3 years ago
return DBA::selectFirst('contact', $fields, ['id' => $id]);
}
/**
* Fetches a contact by a given url
*
* @param string $url profile url
* @param boolean $update true = always update, false = never update, null = update when not found or outdated
* @param array $fields Field list
* @param integer $uid User ID of the contact
* @return array contact array
*/
public static function getByURL(string $url, $update = null, array $fields = [], int $uid = 0)
{
if ($update || is_null($update)) {
$cid = self::getIdForURL($url, $uid, $update);
if (empty($cid)) {
return [];
}
return self::getById($cid, $fields);
}
// Add internal fields
$removal = [];
if (!empty($fields)) {
foreach (['id', 'updated', 'network'] as $internal) {
if (!in_array($internal, $fields)) {
$fields[] = $internal;
$removal[] = $internal;
}
}
}
// We first try the nurl (http://server.tld/nick), most common case
$options = ['order' => ['id']];
$contact = DBA::selectFirst('contact', $fields, ['nurl' => Strings::normaliseLink($url), 'uid' => $uid, 'deleted' => false], $options);
// Then the addr (nick@server.tld)
if (!DBA::isResult($contact)) {
$contact = DBA::selectFirst('contact', $fields, ['addr' => str_replace('acct:', '', $url), 'uid' => $uid, 'deleted' => false], $options);
}
// Then the alias (which could be anything)
if (!DBA::isResult($contact)) {
// The link could be provided as http although we stored it as https
$ssl_url = str_replace('http://', 'https://', $url);
$condition = ['`alias` IN (?, ?, ?) AND `uid` = ? AND NOT `deleted`', $url, Strings::normaliseLink($url), $ssl_url, $uid];
$contact = DBA::selectFirst('contact', $fields, $condition, $options);
}
// Update the contact in the background if needed
if ((($contact['updated'] < DateTimeFormat::utc('now -7 days')) || empty($contact['avatar'])) &&
in_array($contact['network'], Protocol::FEDERATED)) {
Worker::add(PRIORITY_LOW, "UpdateContact", $contact['id'], ($uid == 0 ? 'force' : ''));
}
// Remove the internal fields
foreach ($removal as $internal) {
unset($contact[$internal]);
}
return $contact;
}
/**
* Fetches a contact for a given user by a given url.
* In difference to "getByURL" the function will fetch a public contact when no user contact had been found.
*
* @param string $url profile url
* @param integer $uid User ID of the contact
* @param boolean $update true = always update, false = never update, null = update when not found or outdated
* @param array $fields Field list
* @return array contact array
*/
public static function getByURLForUser(string $url, int $uid = 0, $update = false, array $fields = [])
{
if ($uid != 0) {
$contact = self::getByURL($url, $update, $fields, $uid);
if (!empty($contact)) {
if (!empty($contact['id'])) {
$contact['cid'] = $contact['id'];
$contact['zid'] = 0;
}
return $contact;
}
}
$contact = self::getByURL($url, $update, $fields);
if (!empty($contact['id'])) {
$contact['cid'] = 0;
$contact['zid'] = $contact['id'];
}
return $contact;
}
/**
* Tests if the given contact is a follower
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
*
* @return boolean is the contact id a follower?
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function isFollower($cid, $uid)
{
if (self::isBlockedByUser($cid, $uid)) {
return false;
}
$cdata = self::getPublicAndUserContacID($cid, $uid);
if (empty($cdata['user'])) {
return false;
}
$condition = ['id' => $cdata['user'], 'rel' => [self::FOLLOWER, self::FRIEND]];
return DBA::exists('contact', $condition);
}
/**
* Tests if the given contact url is a follower
*
* @param string $url Contact URL
* @param int $uid User ID
*
* @return boolean is the contact id a follower?
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function isFollowerByURL($url, $uid)
{
$cid = self::getIdForURL($url, $uid, false);
if (empty($cid)) {
return false;
}
return self::isFollower($cid, $uid);
}
/**
* Tests if the given user follow the given contact
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
*
* @return boolean is the contact url being followed?
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function isSharing($cid, $uid)
{
if (self::isBlockedByUser($cid, $uid)) {
return false;
}
$cdata = self::getPublicAndUserContacID($cid, $uid);
if (empty($cdata['user'])) {
return false;
}
$condition = ['id' => $cdata['user'], 'rel' => [self::SHARING, self::FRIEND]];
return DBA::exists('contact', $condition);
}
/**
* Tests if the given user follow the given contact url
*
* @param string $url Contact URL
* @param int $uid User ID
*
* @return boolean is the contact url being followed?
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function isSharingByURL($url, $uid)
{
$cid = self::getIdForURL($url, $uid, false);
if (empty($cid)) {
return false;
}
return self::isSharing($cid, $uid);
}
/**
* Get the basepath for a given contact link
*
* @param string $url The contact link
* @param boolean $dont_update Don't update the contact
*
* @return string basepath
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function getBasepath($url, $dont_update = false)
{
$contact = DBA::selectFirst('contact', ['id', 'baseurl'], ['uid' => 0, 'nurl' => Strings::normaliseLink($url)]);
if (!DBA::isResult($contact)) {
return '';
}
if (!empty($contact['baseurl'])) {
return $contact['baseurl'];
} elseif ($dont_update) {
return '';
}
// Update the existing contact
self::updateFromProbe($contact['id'], '', true);
// And fetch the result
$contact = DBA::selectFirst('contact', ['baseurl'], ['id' => $contact['id']]);
if (empty($contact['baseurl'])) {
Logger::info('No baseurl for contact', ['url' => $url]);
return '';
}
Logger::info('Found baseurl for contact', ['url' => $url, 'baseurl' => $contact['baseurl']]);
return $contact['baseurl'];
}
/**
* Check if the given contact url is on the same server
*
* @param string $url The contact link
*
* @return boolean Is it the same server?
*/
public static function isLocal($url)
{
return Strings::compareLink(self::getBasepath($url, true), DI::baseUrl());
}
/**
* Check if the given contact ID is on the same server
*
* @param string $url The contact link
*
* @return boolean Is it the same server?
*/
public static function isLocalById(int $cid)
{
$contact = DBA::selectFirst('contact', ['url', 'baseurl'], ['id' => $cid]);
if (!DBA::isResult($contact)) {
return false;
}
if (empty($contact['baseurl'])) {
$baseurl = self::getBasepath($contact['url'], true);
} else {
$baseurl = $contact['baseurl'];
}
return Strings::compareLink($baseurl, DI::baseUrl());
}
/**
* Returns the public contact id of the given user id
*
* @param integer $uid User ID
*
* @return integer|boolean Public contact id for given user id
* @throws \Exception
*/
public static function getPublicIdByUserId($uid)
{
$self = DBA::selectFirst('contact', ['url'], ['self' => true, 'uid' => $uid]);
if (!DBA::isResult($self)) {
return false;
}
return self::getIdForURL($self['url'], 0, false);
}
/**
* Returns the contact id for the user and the public contact id for a given contact id
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
*
* @return array with public and user's contact id
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function getPublicAndUserContacID($cid, $uid)
{
if (empty($uid) || empty($cid)) {
return [];
}
$contact = DBA::selectFirst('contact', ['id', 'uid', 'url'], ['id' => $cid]);
if (!DBA::isResult($contact)) {
return [];
}
// We quit when the user id don't match the user id of the provided contact
if (($contact['uid'] != $uid) && ($contact['uid'] != 0)) {
return [];
}
if ($contact['uid'] != 0) {
$pcid = Contact::getIdForURL($contact['url'], 0, false, ['url' => $contact['url']]);
if (empty($pcid)) {
return [];
}
$ucid = $contact['id'];
} else {
$pcid = $contact['id'];
$ucid = Contact::getIdForURL($contact['url'], $uid, false);
}
return ['public' => $pcid, 'user' => $ucid];
}
/**
* Returns contact details for a given contact id in combination with a user id
*
* @param int $cid A contact ID
* @param int $uid The User ID
* @param array $fields The selected fields for the contact
*
* @return array The contact details
*
* @throws \Exception
*/
public static function getContactForUser($cid, $uid, array $fields = [])
{
$contact = DBA::selectFirst('contact', $fields, ['id' => $cid, 'uid' => $uid]);
if (!DBA::isResult($contact)) {
return [];
} else {
return $contact;
}
}
/**
* Block contact id for user id
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
* @param boolean $blocked Is the contact blocked or unblocked?
* @throws \Exception
*/
public static function setBlockedForUser($cid, $uid, $blocked)
{
$cdata = self::getPublicAndUserContacID($cid, $uid);
if (empty($cdata)) {
return;
}
if ($cdata['user'] != 0) {
DBA::update('contact', ['blocked' => $blocked], ['id' => $cdata['user'], 'pending' => false]);
}
DBA::update('user-contact', ['blocked' => $blocked], ['cid' => $cdata['public'], 'uid' => $uid], true);
}
/**
* Returns "block" state for contact id and user id
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
*
* @return boolean is the contact id blocked for the given user?
* @throws \Exception
*/
public static function isBlockedByUser($cid, $uid)
{
$cdata = self::getPublicAndUserContacID($cid, $uid);
if (empty($cdata)) {
return;
}
$public_blocked = false;
if (!empty($cdata['public'])) {
$public_contact = DBA::selectFirst('user-contact', ['blocked'], ['cid' => $cdata['public'], 'uid' => $uid]);
if (DBA::isResult($public_contact)) {
$public_blocked = $public_contact['blocked'];
}
}
$user_blocked = $public_blocked;
if (!empty($cdata['user'])) {
$user_contact = DBA::selectFirst('contact', ['blocked'], ['id' => $cdata['user'], 'pending' => false]);
if (DBA::isResult($user_contact)) {
$user_blocked = $user_contact['blocked'];
}
}
if ($user_blocked != $public_blocked) {
DBA::update('user-contact', ['blocked' => $user_blocked], ['cid' => $cdata['public'], 'uid' => $uid], true);
}
return $user_blocked;
}
/**
* Ignore contact id for user id
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
* @param boolean $ignored Is the contact ignored or unignored?
* @throws \Exception
*/
public static function setIgnoredForUser($cid, $uid, $ignored)
{
$cdata = self::getPublicAndUserContacID($cid, $uid);
if (empty($cdata)) {
return;
}
if ($cdata['user'] != 0) {
DBA::update('contact', ['readonly' => $ignored], ['id' => $cdata['user'], 'pending' => false]);
}
DBA::update('user-contact', ['ignored' => $ignored], ['cid' => $cdata['public'], 'uid' => $uid], true);
}
/**
* Returns "ignore" state for contact id and user id
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
*
* @return boolean is the contact id ignored for the given user?
* @throws \Exception
*/
public static function isIgnoredByUser($cid, $uid)
{
$cdata = self::getPublicAndUserContacID($cid, $uid);
if (empty($cdata)) {
return;
}
$public_ignored = false;
if (!empty($cdata['public'])) {
$public_contact = DBA::selectFirst('user-contact', ['ignored'], ['cid' => $cdata['public'], 'uid' => $uid]);
if (DBA::isResult($public_contact)) {
$public_ignored = $public_contact['ignored'];
}
}
$user_ignored = $public_ignored;
if (!empty($cdata['user'])) {
$user_contact = DBA::selectFirst('contact', ['readonly'], ['id' => $cdata['user'], 'pending' => false]);
if (DBA::isResult($user_contact)) {
$user_ignored = $user_contact['readonly'];
}
}
if ($user_ignored != $public_ignored) {
DBA::update('user-contact', ['ignored' => $user_ignored], ['cid' => $cdata['public'], 'uid' => $uid], true);
}
return $user_ignored;
}
/**
* Set "collapsed" for contact id and user id
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
* @param boolean $collapsed are the contact's posts collapsed or uncollapsed?
* @throws \Exception
*/
public static function setCollapsedForUser($cid, $uid, $collapsed)
{
$cdata = self::getPublicAndUserContacID($cid, $uid);
if (empty($cdata)) {
return;
}
DBA::update('user-contact', ['collapsed' => $collapsed], ['cid' => $cdata['public'], 'uid' => $uid], true);
}
/**
* Returns "collapsed" state for contact id and user id
*
* @param int $cid Either public contact id or user's contact id
* @param int $uid User ID
*
* @return boolean is the contact id blocked for the given user?
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function isCollapsedByUser($cid, $uid)
{
$cdata = self::getPublicAndUserContacID($cid, $uid);
if (empty($cdata)) {
return;
}
$collapsed = false;
if (!empty($cdata['public'])) {
$public_contact = DBA::selectFirst('user-contact', ['collapsed'], ['cid' => $cdata['public'], 'uid' => $uid]);
if (DBA::isResult($public_contact)) {
$collapsed = $public_contact['collapsed'];
}
}
return $collapsed;
}
/**
* Returns a list of contacts belonging in a group
*
* @param int $gid
* @return array
* @throws \Exception
*/
public static function getByGroupId($gid)
{
$return = [];
if (intval($gid)) {
$stmt = DBA::p('SELECT `group_member`.`contact-id`, `contact`.*
FROM `contact`
INNER JOIN `group_member`
ON `contact`.`id` = `group_member`.`contact-id`
WHERE `gid` = ?
AND `contact`.`uid` = ?
AND NOT `contact`.`self`
AND NOT `contact`.`deleted`
AND NOT `contact`.`blocked`
AND NOT `contact`.`pending`
ORDER BY `contact`.`name` ASC',
$gid,
local_user()
);
if (DBA::isResult($stmt)) {
$return = DBA::toArray($stmt);
}
}
return $return;
}
/**
* Creates the self-contact for the provided user id
*
* @param int $uid
* @return bool Operation success
* @throws HTTPException\InternalServerErrorException
*/
public static function createSelfFromUserId($uid)
{
// Only create the entry if it doesn't exist yet
if (DBA::exists('contact', ['uid' => $uid, 'self' => true])) {
return true;
}
$user = DBA::selectFirst('user', ['uid', 'username', 'nickname'], ['uid' => $uid]);
if (!DBA::isResult($user)) {
return false;
}
$return = DBA::insert('contact', [
'uid' => $user['uid'],
'created' => DateTimeFormat::utcNow(),
'self' => 1,
'name' => $user['username'],
'nick' => $user['nickname'],
'photo' => DI::baseUrl() . '/photo/profile/' . $user['uid'] . '.jpg',
'thumb' => DI::baseUrl() . '/photo/avatar/' . $user['uid'] . '.jpg',
'micro' => DI::baseUrl() . '/photo/micro/' . $user['uid'] . '.jpg',
'blocked' => 0,
'pending' => 0,
'url' => DI::baseUrl() . '/profile/' . $user['nickname'],
'nurl' => Strings::normaliseLink(DI::baseUrl() . '/profile/' . $user['nickname']),
'addr' => $user['nickname'] . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3),
'request' => DI::baseUrl() . '/dfrn_request/' . $user['nickname'],
'notify' => DI::baseUrl() . '/dfrn_notify/' . $user['nickname'],
'poll' => DI::baseUrl() . '/dfrn_poll/' . $user['nickname'],
'confirm' => DI::baseUrl() . '/dfrn_confirm/' . $user['nickname'],
'poco' => DI::baseUrl() . '/poco/' . $user['nickname'],
'name-date' => DateTimeFormat::utcNow(),
'uri-date' => DateTimeFormat::utcNow(),
'avatar-date' => DateTimeFormat::utcNow(),
'closeness' => 0
]);
return $return;
}
/**
* Updates the self-contact for the provided user id
*
* @param int $uid
* @param boolean $update_avatar Force the avatar update
* @throws HTTPException\InternalServerErrorException
*/
public static function updateSelfFromUserID($uid, $update_avatar = false)
{
2 years ago
$fields = ['id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar',
'xmpp', 'contact-type', 'forum', 'prv', 'avatar-date', 'url', 'nurl', 'unsearchable',
'photo', 'thumb', 'micro', 'addr', 'request', 'notify', 'poll', 'confirm', 'poco'];
$self = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]);
if (!DBA::isResult($self)) {
return;
}
$fields = ['nickname', 'page-flags', 'account-type'];
$user = DBA::selectFirst('user', $fields, ['uid' => $uid]);
if (!DBA::isResult($user)) {
return;
}
$fields = ['name', 'photo', 'thumb', 'about', 'address', 'locality', 'region',
'country-name', 'pub_keywords', 'xmpp', 'net-publish'];
$profile = DBA::selectFirst('profile', $fields, ['uid' => $uid]);
if (!DBA::isResult($profile)) {
return;
}