Merge remote-tracking branch 'upstream/develop' into sanitize-gcontact

This commit is contained in:
Michael 2019-10-15 10:10:12 +00:00
commit f1e7d97b8c
1219 changed files with 241816 additions and 178090 deletions

View file

@ -7,14 +7,15 @@
namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Content\Text\HTML;
use Friendica\Core\Logger;
use Friendica\Core\Config;
use Friendica\Database\DBA;
use Friendica\Protocol\ActivityPub;
use Friendica\Util\Network;
use Friendica\Util\JsonLD;
use Friendica\Util\DateTimeFormat;
use Friendica\Content\Text\HTML;
require_once 'boot.php';
use Friendica\Util\Strings;
class APContact extends BaseObject
{
@ -22,20 +23,30 @@ class APContact extends BaseObject
* Resolves the profile url from the address by using webfinger
*
* @param string $addr profile address (user@domain.tld)
* @return string url
* @param string $url profile URL. When set then we return "true" when this profile url can be found at the address
* @return string|boolean url
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function addrToUrl($addr)
private static function addrToUrl($addr, $url = null)
{
$addr_parts = explode('@', $addr);
if (count($addr_parts) != 2) {
return false;
}
$xrd_timeout = Config::get('system', 'xrd_timeout');
$webfinger = 'https://' . $addr_parts[1] . '/.well-known/webfinger?resource=acct:' . urlencode($addr);
$curlResult = Network::curl($webfinger, false, $redirects, ['accept_content' => 'application/jrd+json,application/json']);
$curlResult = Network::curl($webfinger, false, ['timeout' => $xrd_timeout, 'accept_content' => 'application/jrd+json,application/json']);
if (!$curlResult->isSuccess() || empty($curlResult->getBody())) {
return false;
$webfinger = Strings::normaliseLink($webfinger);
$curlResult = Network::curl($webfinger, false, ['timeout' => $xrd_timeout, 'accept_content' => 'application/jrd+json,application/json']);
if (!$curlResult->isSuccess() || empty($curlResult->getBody())) {
return false;
}
}
$data = json_decode($curlResult->getBody(), true);
@ -45,11 +56,15 @@ class APContact extends BaseObject
}
foreach ($data['links'] as $link) {
if (!empty($url) && !empty($link['href']) && ($link['href'] == $url)) {
return true;
}
if (empty($link['href']) || empty($link['rel']) || empty($link['type'])) {
continue;
}
if (($link['rel'] == 'self') && ($link['type'] == 'application/activity+json')) {
if (empty($url) && ($link['rel'] == 'self') && ($link['type'] == 'application/activity+json')) {
return $link['href'];
}
}
@ -61,8 +76,10 @@ class APContact extends BaseObject
* Fetches a profile from a given url
*
* @param string $url profile url
* @param boolean $update true = always update, false = never update, null = update when not found
* @param boolean $update true = always update, false = never update, null = update when not found or outdated
* @return array profile array
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function getByURL($url, $update = null)
{
@ -70,66 +87,83 @@ class APContact extends BaseObject
return false;
}
$fetched_contact = false;
if (empty($update)) {
if (is_null($update)) {
$ref_update = DateTimeFormat::utc('now - 1 month');
} else {
$ref_update = DBA::NULL_DATETIME;
}
$apcontact = DBA::selectFirst('apcontact', [], ['url' => $url]);
if (DBA::isResult($apcontact)) {
return $apcontact;
if (!DBA::isResult($apcontact)) {
$apcontact = DBA::selectFirst('apcontact', [], ['alias' => $url]);
}
$apcontact = DBA::selectFirst('apcontact', [], ['alias' => $url]);
if (DBA::isResult($apcontact)) {
return $apcontact;
if (!DBA::isResult($apcontact)) {
$apcontact = DBA::selectFirst('apcontact', [], ['addr' => $url]);
}
$apcontact = DBA::selectFirst('apcontact', [], ['addr' => $url]);
if (DBA::isResult($apcontact)) {
if (DBA::isResult($apcontact) && ($apcontact['updated'] > $ref_update) && !empty($apcontact['pubkey'])) {
return $apcontact;
}
if (!is_null($update)) {
return false;
return DBA::isResult($apcontact) ? $apcontact : false;
}
if (DBA::isResult($apcontact)) {
$fetched_contact = $apcontact;
}
}
if (empty(parse_url($url, PHP_URL_SCHEME))) {
$url = self::addrToUrl($url);
if (empty($url)) {
return false;
return $fetched_contact;
}
}
$data = ActivityPub::fetchContent($url);
if (empty($data)) {
return false;
return $fetched_contact;
}
$compacted = JsonLD::compact($data);
if (empty($compacted['@id'])) {
return $fetched_contact;
}
$apcontact = [];
$apcontact['url'] = $compacted['@id'];
$apcontact['uuid'] = JsonLD::fetchElement($compacted, 'diaspora:guid');
$apcontact['uuid'] = JsonLD::fetchElement($compacted, 'diaspora:guid', '@value');
$apcontact['type'] = str_replace('as:', '', JsonLD::fetchElement($compacted, '@type'));
$apcontact['following'] = JsonLD::fetchElement($compacted, 'as:following', '@id');
$apcontact['followers'] = JsonLD::fetchElement($compacted, 'as:followers', '@id');
$apcontact['inbox'] = JsonLD::fetchElement($compacted, 'ldp:inbox', '@id');
self::unarchiveInbox($apcontact['inbox'], false);
$apcontact['outbox'] = JsonLD::fetchElement($compacted, 'as:outbox', '@id');
$apcontact['sharedinbox'] = '';
if (!empty($compacted['as:endpoints'])) {
$apcontact['sharedinbox'] = JsonLD::fetchElement($compacted['as:endpoints'], 'as:sharedInbox', '@id');
self::unarchiveInbox($apcontact['sharedinbox'], true);
}
$apcontact['nick'] = JsonLD::fetchElement($compacted, 'as:preferredUsername');
$apcontact['name'] = JsonLD::fetchElement($compacted, 'as:name');
$apcontact['nick'] = JsonLD::fetchElement($compacted, 'as:preferredUsername', '@value') ?? '';
$apcontact['name'] = JsonLD::fetchElement($compacted, 'as:name', '@value');
if (empty($apcontact['name'])) {
$apcontact['name'] = $apcontact['nick'];
}
$apcontact['about'] = HTML::toBBCode(JsonLD::fetchElement($compacted, 'as:summary'));
$apcontact['about'] = HTML::toBBCode(JsonLD::fetchElement($compacted, 'as:summary', '@value'));
$apcontact['photo'] = JsonLD::fetchElement($compacted, 'as:icon', '@id');
if (is_array($apcontact['photo'])) {
if (is_array($apcontact['photo']) || !empty($compacted['as:icon']['as:url']['@id'])) {
$apcontact['photo'] = JsonLD::fetchElement($compacted['as:icon'], 'as:url', '@id');
}
@ -138,22 +172,41 @@ class APContact extends BaseObject
$apcontact['alias'] = JsonLD::fetchElement($compacted['as:url'], 'as:href', '@id');
}
if (empty($apcontact['url']) || empty($apcontact['inbox'])) {
return false;
// Quit if none of the basic values are set
if (empty($apcontact['url']) || empty($apcontact['inbox']) || empty($apcontact['type'])) {
return $fetched_contact;
}
// Quit if this doesn't seem to be an account at all
if (!in_array($apcontact['type'], ActivityPub::ACCOUNT_TYPES)) {
return $fetched_contact;
}
$parts = parse_url($apcontact['url']);
unset($parts['scheme']);
unset($parts['path']);
$apcontact['addr'] = $apcontact['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts));
$apcontact['pubkey'] = trim(JsonLD::fetchElement($compacted, 'w3id:publicKey', 'w3id:publicKeyPem'));
if (!empty($apcontact['nick'])) {
$apcontact['addr'] = $apcontact['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts));
} else {
$apcontact['addr'] = '';
}
if (!empty($compacted['w3id:publicKey'])) {
$apcontact['pubkey'] = trim(JsonLD::fetchElement($compacted['w3id:publicKey'], 'w3id:publicKeyPem', '@value'));
}
$apcontact['manually-approve'] = (int)JsonLD::fetchElement($compacted, 'as:manuallyApprovesFollowers');
if (!empty($compacted['as:generator'])) {
$apcontact['baseurl'] = JsonLD::fetchElement($compacted['as:generator'], 'as:url', '@id');
$apcontact['generator'] = JsonLD::fetchElement($compacted['as:generator'], 'as:name', '@value');
}
// To-Do
// manuallyApprovesFollowers
// Unhandled
// @context, tag, attachment, image, nomadicLocations, signature, following, followers, featured, movedTo, liked
// tag, attachment, image, nomadicLocations, signature, featured, movedTo, liked
// Unhandled from Misskey
// sharedInbox, isCat
@ -161,13 +214,18 @@ class APContact extends BaseObject
// Unhandled from Kroeg
// kroeg:blocks, updated
// Check if the address is resolvable
if (self::addrToUrl($apcontact['addr']) == $apcontact['url']) {
$parts = parse_url($apcontact['url']);
unset($parts['path']);
$apcontact['baseurl'] = Network::unparseURL($parts);
$parts = parse_url($apcontact['url']);
unset($parts['path']);
$baseurl = Network::unparseURL($parts);
// Check if the address is resolvable or the profile url is identical with the base url of the system
if (self::addrToUrl($apcontact['addr'], $apcontact['url']) || Strings::compareLink($apcontact['url'], $baseurl)) {
$apcontact['baseurl'] = $baseurl;
} else {
$apcontact['addr'] = null;
}
if (empty($apcontact['baseurl'])) {
$apcontact['baseurl'] = null;
}
@ -179,21 +237,36 @@ class APContact extends BaseObject
DBA::update('apcontact', $apcontact, ['url' => $url], true);
// Update some data in the contact table with various ways to catch them all
$contact_fields = ['name' => $apcontact['name'], 'about' => $apcontact['about']];
DBA::update('contact', $contact_fields, ['nurl' => normalise_link($url)]);
$contacts = DBA::select('contact', ['uid', 'id'], ['nurl' => normalise_link($url)]);
while ($contact = DBA::fetch($contacts)) {
Contact::updateAvatar($apcontact['photo'], $contact['uid'], $contact['id']);
// We delete the old entry when the URL is changed
if (($url != $apcontact['url']) && DBA::exists('apcontact', ['url' => $url]) && DBA::exists('apcontact', ['url' => $apcontact['url']])) {
DBA::delete('apcontact', ['url' => $url]);
}
DBA::close($contacts);
// Update the gcontact table
DBA::update('gcontact', $contact_fields, ['nurl' => normalise_link($url)]);
logger('Updated profile for ' . $url, LOGGER_DEBUG);
Logger::log('Updated profile for ' . $url, Logger::DEBUG);
return $apcontact;
}
/**
* Unarchive inboxes
*
* @param string $url inbox url
*/
private static function unarchiveInbox($url, $shared)
{
if (empty($url)) {
return;
}
$now = DateTimeFormat::utcNow();
$fields = ['archive' => false, 'success' => $now, 'shared' => $shared];
if (!DBA::exists('inbox-status', ['url' => $url])) {
$fields = array_merge($fields, ['url' => $url, 'created' => $now]);
DBA::insert('inbox-status', $fields);
} else {
DBA::update('inbox-status', $fields, ['url' => $url]);
}
}
}

311
src/Model/Attach.php Normal file
View file

@ -0,0 +1,311 @@
<?php
/**
* @file src/Model/Attach.php
* @brief This file contains the Attach class for database interface
*/
namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Core\StorageManager;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Database\DBStructure;
use Friendica\Model\Storage\IStorage;
use Friendica\Object\Image;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Mimetype;
use Friendica\Util\Security;
/**
* Class to handle attach dabatase table
*/
class Attach extends BaseObject
{
/**
* @brief Return a list of fields that are associated with the attach table
*
* @return array field list
* @throws \Exception
*/
private static function getFields()
{
$allfields = DBStructure::definition(self::getApp()->getBasePath(), false);
$fields = array_keys($allfields['attach']['fields']);
array_splice($fields, array_search('data', $fields), 1);
return $fields;
}
/**
* @brief Select rows from the attach table and return them as array
*
* @param array $fields Array of selected fields, empty for all
* @param array $conditions Array of fields for conditions
* @param array $params Array of several parameters
*
* @return array
*
* @throws \Exception
* @see \Friendica\Database\DBA::selectToArray
*/
public static function selectToArray(array $fields = [], array $conditions = [], array $params = [])
{
if (empty($fields)) {
$fields = self::getFields();
}
return DBA::selectToArray('attach', $fields, $conditions, $params);
}
/**
* @brief Retrieve a single record from the attach table
*
* @param array $fields Array of selected fields, empty for all
* @param array $conditions Array of fields for conditions
* @param array $params Array of several parameters
*
* @return bool|array
*
* @throws \Exception
* @see \Friendica\Database\DBA::select
*/
public static function selectFirst(array $fields = [], array $conditions = [], array $params = [])
{
if (empty($fields)) {
$fields = self::getFields();
}
return DBA::selectFirst('attach', $fields, $conditions, $params);
}
/**
* @brief Check if attachment with given conditions exists
*
* @param array $conditions Array of extra conditions
*
* @return boolean
* @throws \Exception
*/
public static function exists(array $conditions)
{
return DBA::exists('attach', $conditions);
}
/**
* @brief Retrive a single record given the ID
*
* @param int $id Row id of the record
*
* @return bool|array
*
* @throws \Exception
* @see \Friendica\Database\DBA::select
*/
public static function getById($id)
{
return self::selectFirst([], ['id' => $id]);
}
/**
* @brief Retrive a single record given the ID
*
* @param int $id Row id of the record
*
* @return bool|array
*
* @throws \Exception
* @see \Friendica\Database\DBA::select
*/
public static function getByIdWithPermission($id)
{
$r = self::selectFirst(['uid'], ['id' => $id]);
if ($r === false) {
return false;
}
$sql_acl = Security::getPermissionsSQLByUserId($r['uid']);
$conditions = [
'`id` = ?' . $sql_acl,
$id
];
$item = self::selectFirst([], $conditions);
return $item;
}
/**
* @brief Get file data for given row id. null if row id does not exist
*
* @param array $item Attachment data. Needs at least 'id', 'backend-class', 'backend-ref'
*
* @return string file data
* @throws \Exception
*/
public static function getData($item)
{
if ($item['backend-class'] == '') {
// legacy data storage in 'data' column
$i = self::selectFirst(['data'], ['id' => $item['id']]);
if ($i === false) {
return null;
}
return $i['data'];
} else {
$backendClass = $item['backend-class'];
$backendRef = $item['backend-ref'];
return $backendClass::get($backendRef);
}
}
/**
* @brief Store new file metadata in db and binary in default backend
*
* @param string $data Binary data
* @param integer $uid User ID
* @param string $filename Filename
* @param string $filetype Mimetype. optional, default = ''
* @param integer $filesize File size in bytes. optional, default = null
* @param string $allow_cid Permissions, allowed contacts. optional, default = ''
* @param string $allow_gid Permissions, allowed groups. optional, default = ''
* @param string $deny_cid Permissions, denied contacts.optional, default = ''
* @param string $deny_gid Permissions, denied greoup.optional, default = ''
*
* @return boolean/integer Row id on success, False on errors
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function store($data, $uid, $filename, $filetype = '' , $filesize = null, $allow_cid = '', $allow_gid = '', $deny_cid = '', $deny_gid = '')
{
if ($filetype === '') {
$filetype = Mimetype::getContentType($filename);
}
if (is_null($filesize)) {
$filesize = strlen($data);
}
/** @var IStorage $backend_class */
$backend_class = StorageManager::getBackend();
$backend_ref = '';
if ($backend_class !== '') {
$backend_ref = $backend_class::put($data);
$data = '';
}
$hash = System::createGUID(64);
$created = DateTimeFormat::utcNow();
$fields = [
'uid' => $uid,
'hash' => $hash,
'filename' => $filename,
'filetype' => $filetype,
'filesize' => $filesize,
'data' => $data,
'created' => $created,
'edited' => $created,
'allow_cid' => $allow_cid,
'allow_gid' => $allow_gid,
'deny_cid' => $deny_cid,
'deny_gid' => $deny_gid,
'backend-class' => $backend_class,
'backend-ref' => $backend_ref
];
$r = DBA::insert('attach', $fields);
if ($r === true) {
return DBA::lastInsertId();
}
return $r;
}
/**
* @brief Store new file metadata in db and binary in default backend from existing file
*
* @param $src
* @param $uid
* @param string $filename
* @param string $allow_cid
* @param string $allow_gid
* @param string $deny_cid
* @param string $deny_gid
* @return boolean True on success
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function storeFile($src, $uid, $filename = '', $allow_cid = '', $allow_gid = '', $deny_cid = '', $deny_gid = '')
{
if ($filename === '') {
$filename = basename($src);
}
$data = @file_get_contents($src);
return self::store($data, $uid, $filename, '', null, $allow_cid, $allow_gid, $deny_cid, $deny_gid);
}
/**
* @brief Update an attached file
*
* @param array $fields Contains the fields that are updated
* @param array $conditions Condition array with the key values
* @param Image $img Image data to update. Optional, default null.
* @param array|boolean $old_fields Array with the old field values that are about to be replaced (true = update on duplicate)
*
* @return boolean Was the update successful?
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @see \Friendica\Database\DBA::update
*/
public static function update($fields, $conditions, Image $img = null, array $old_fields = [])
{
if (!is_null($img)) {
// get items to update
$items = self::selectToArray(['backend-class','backend-ref'], $conditions);
foreach($items as $item) {
/** @var IStorage $backend_class */
$backend_class = (string)$item['backend-class'];
if ($backend_class !== '') {
$fields['backend-ref'] = $backend_class::put($img->asString(), $item['backend-ref']);
} else {
$fields['data'] = $img->asString();
}
}
}
$fields['edited'] = DateTimeFormat::utcNow();
return DBA::update('attach', $fields, $conditions, $old_fields);
}
/**
* @brief Delete info from table and data from storage
*
* @param array $conditions Field condition(s)
* @param array $options Options array, Optional
*
* @return boolean
*
* @throws \Exception
* @see \Friendica\Database\DBA::delete
*/
public static function delete(array $conditions, array $options = [])
{
// get items to delete data info
$items = self::selectToArray(['backend-class','backend-ref'], $conditions);
foreach($items as $item) {
/** @var IStorage $backend_class */
$backend_class = (string)$item['backend-class'];
if ($backend_class !== '') {
$backend_class::delete($item['backend-ref']);
}
}
return DBA::delete('attach', $conditions, $options);
}
}

131
src/Model/Config/Config.php Normal file
View file

@ -0,0 +1,131 @@
<?php
namespace Friendica\Model\Config;
/**
* The Config model backend, which is using the general DB-model backend for configs
*/
class Config extends DbaConfig
{
/**
* Loads all configuration values and returns the loaded category as an array.
*
* @param string|null $cat The category of the configuration values to load
*
* @return array The config array
*
* @throws \Exception In case DB calls are invalid
*/
public function load(string $cat = null)
{
$return = [];
if (empty($cat)) {
$configs = $this->dba->select('config', ['cat', 'v', 'k']);
} else {
$configs = $this->dba->select('config', ['cat', 'v', 'k'], ['cat' => $cat]);
}
while ($config = $this->dba->fetch($configs)) {
$key = $config['k'];
$value = $this->toConfigValue($config['v']);
// just save it in case it is set
if (isset($value)) {
$return[$config['cat']][$key] = $value;
}
}
$this->dba->close($configs);
return $return;
}
/**
* Get a particular, system-wide config variable out of the DB with the
* given category name ($cat) and a key ($key).
*
* Note: Boolean variables are defined as 0/1 in the database
*
* @param string $cat The category of the configuration value
* @param string $key The configuration key to query
*
* @return array|string|null Stored value or null if it does not exist
*
* @throws \Exception In case DB calls are invalid
*/
public function get(string $cat, string $key)
{
if (!$this->isConnected()) {
return null;
}
$config = $this->dba->selectFirst('config', ['v'], ['cat' => $cat, 'k' => $key]);
if ($this->dba->isResult($config)) {
$value = $this->toConfigValue($config['v']);
// just return it in case it is set
if (isset($value)) {
return $value;
}
}
return null;
}
/**
* Stores a config value ($value) in the category ($cat) under the key ($key).
*
* Note: Please do not store booleans - convert to 0/1 integer values!
*
* @param string $cat The category of the configuration value
* @param string $key The configuration key to set
* @param mixed $value The value to store
*
* @return bool Operation success
*
* @throws \Exception In case DB calls are invalid
*/
public function set(string $cat, string $key, $value)
{
if (!$this->isConnected()) {
return false;
}
// We store our setting values in a string variable.
// So we have to do the conversion here so that the compare below works.
// The exception are array values.
$compare_value = (!is_array($value) ? (string)$value : $value);
$stored_value = $this->get($cat, $key);
if (isset($stored_value) && ($stored_value === $compare_value)) {
return true;
}
$dbvalue = $this->toDbValue($value);
$result = $this->dba->update('config', ['v' => $dbvalue], ['cat' => $cat, 'k' => $key], true);
return $result;
}
/**
* Removes the configured value from the database.
*
* @param string $cat The category of the configuration value
* @param string $key The configuration key to delete
*
* @return bool Operation success
*
* @throws \Exception In case DB calls are invalid
*/
public function delete(string $cat, string $key)
{
if (!$this->isConnected()) {
return false;
}
return $this->dba->delete('config', ['cat' => $cat, 'k' => $key]);
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace Friendica\Model\Config;
use Friendica\Database\Database;
/**
* The DB-based model of (P-)Config values
* Encapsulates db-calls in case of config queries
*/
abstract class DbaConfig
{
/** @var Database */
protected $dba;
/**
* @param Database $dba The database connection of this model
*/
public function __construct(Database $dba)
{
$this->dba = $dba;
}
/**
* Checks if the model is currently connected
*
* @return bool
*/
public function isConnected()
{
return $this->dba->isConnected();
}
/**
* Formats a DB value to a config value
* - null = The db-value isn't set
* - bool = The db-value is either '0' or '1'
* - array = The db-value is a serialized array
* - string = The db-value is a string
*
* Keep in mind that there aren't any numeric/integer config values in the database
*
* @param null|string $value
*
* @return null|array|string
*/
protected function toConfigValue($value)
{
if (!isset($value)) {
return null;
}
switch (true) {
// manage array value
case preg_match("|^a:[0-9]+:{.*}$|s", $value):
return unserialize($value);
default:
return $value;
}
}
/**
* Formats a config value to a DB value (string)
*
* @param mixed $value
*
* @return string
*/
protected function toDbValue($value)
{
// if not set, save an empty string
if (!isset($value)) {
return '';
}
switch (true) {
// manage arrays
case is_array($value):
return serialize($value);
default:
return (string)$value;
}
}
}

View file

@ -0,0 +1,135 @@
<?php
namespace Friendica\Model\Config;
/**
* The Config model backend for users, which is using the general DB-model backend for user-configs
*/
class PConfig extends DbaConfig
{
/**
* Loads all configuration values and returns the loaded category as an array.
*
* @param int $uid The id of the user to load
* @param string|null $cat The category of the configuration values to load
*
* @return array The config array
*
* @throws \Exception In case DB calls are invalid
*/
public function load(int $uid, string $cat = null)
{
$return = [];
if (empty($cat)) {
$configs = $this->dba->select('pconfig', ['cat', 'v', 'k'], ['uid' => $uid]);
} else {
$configs = $this->dba->select('pconfig', ['cat', 'v', 'k'], ['cat' => $cat, 'uid' => $uid]);
}
while ($config = $this->dba->fetch($configs)) {
$key = $config['k'];
$value = $this->toConfigValue($config['v']);
// just save it in case it is set
if (isset($value)) {
$return[$config['cat']][$key] = $value;
}
}
$this->dba->close($configs);
return $return;
}
/**
* Get a particular user config variable out of the DB with the
* given category name ($cat) and a key ($key).
*
* Note: Boolean variables are defined as 0/1 in the database
*
* @param int $uid The id of the user to load
* @param string $cat The category of the configuration value
* @param string $key The configuration key to query
*
* @return array|string|null Stored value or null if it does not exist
*
* @throws \Exception In case DB calls are invalid
*/
public function get(int $uid, string $cat, string $key)
{
if (!$this->isConnected()) {
return null;
}
$config = $this->dba->selectFirst('pconfig', ['v'], ['uid' => $uid, 'cat' => $cat, 'k' => $key]);
if ($this->dba->isResult($config)) {
$value = $this->toConfigValue($config['v']);
// just return it in case it is set
if (isset($value)) {
return $value;
}
}
return null;
}
/**
* Stores a config value ($value) in the category ($cat) under the key ($key) for a
* given user ($uid).
*
* Note: Please do not store booleans - convert to 0/1 integer values!
*
* @param int $uid The id of the user to load
* @param string $cat The category of the configuration value
* @param string $key The configuration key to set
* @param mixed $value The value to store
*
* @return bool Operation success
*
* @throws \Exception In case DB calls are invalid
*/
public function set(int $uid, string $cat, string $key, $value)
{
if (!$this->isConnected()) {
return false;
}
// We store our setting values in a string variable.
// So we have to do the conversion here so that the compare below works.
// The exception are array values.
$compare_value = (!is_array($value) ? (string)$value : $value);
$stored_value = $this->get($uid, $cat, $key);
if (isset($stored_value) && ($stored_value === $compare_value)) {
return true;
}
$dbvalue = $this->toDbValue($value);
$result = $this->dba->update('pconfig', ['v' => $dbvalue], ['uid' => $uid, 'cat' => $cat, 'k' => $key], true);
return $result;
}
/**
* Removes the configured value of the given user.
*
* @param int $uid The id of the user to load
* @param string $cat The category of the configuration value
* @param string $key The configuration key to delete
*
* @return bool Operation success
*
* @throws \Exception In case DB calls are invalid
*/
public function delete(int $uid, string $cat, string $key)
{
if (!$this->isConnected()) {
return false;
}
return $this->dba->delete('pconfig', ['uid' => $uid, 'cat' => $cat, 'k' => $key]);
}
}

File diff suppressed because it is too large Load diff

View file

@ -5,12 +5,11 @@
namespace Friendica\Model;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Database\DBA;
use Friendica\Util\DateTimeFormat;
require_once "include/dba.php";
class Conversation
{
/*
@ -36,6 +35,7 @@ class Conversation
*
* @param array $arr Item array with conversation data
* @return array Item array with removed conversation data
* @throws \Exception
*/
public static function insert(array $arr)
{
@ -77,18 +77,18 @@ class Conversation
}
// Update structure data all the time but the source only when its from a better protocol.
if (empty($conversation['source']) || (!empty($old_conv['source']) &&
($old_conv['protocol'] < defaults($conversation, 'protocol', PARCEL_UNKNOWN)))) {
($old_conv['protocol'] < defaults($conversation, 'protocol', self::PARCEL_UNKNOWN)))) {
unset($conversation['protocol']);
unset($conversation['source']);
}
if (!DBA::update('conversation', $conversation, ['item-uri' => $conversation['item-uri']], $old_conv)) {
logger('Conversation: update for ' . $conversation['item-uri'] . ' from ' . $old_conv['protocol'] . ' to ' . $conversation['protocol'] . ' failed',
LOGGER_DEBUG);
Logger::log('Conversation: update for ' . $conversation['item-uri'] . ' from ' . $old_conv['protocol'] . ' to ' . $conversation['protocol'] . ' failed',
Logger::DEBUG);
}
} else {
if (!DBA::insert('conversation', $conversation, true)) {
logger('Conversation: insert for ' . $conversation['item-uri'] . ' (protocol ' . $conversation['protocol'] . ') failed',
LOGGER_DEBUG);
Logger::log('Conversation: insert for ' . $conversation['item-uri'] . ' (protocol ' . $conversation['protocol'] . ') failed',
Logger::DEBUG);
}
}
}

View file

@ -7,18 +7,17 @@ namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Addon;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\PConfig;
use Friendica\Core\Renderer;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Map;
require_once 'boot.php';
require_once 'include/dba.php';
require_once 'include/items.php';
use Friendica\Util\Strings;
use Friendica\Util\XML;
/**
* @brief functions for interacting with the event database table
@ -34,13 +33,13 @@ class Event extends BaseObject
$bd_format = L10n::t('l F d, Y \@ g:i A'); // Friday January 18, 2011 @ 8 AM.
$event_start = day_translate(
$event_start = L10n::getDay(
!empty($event['adjust']) ?
DateTimeFormat::local($event['start'], $bd_format) : DateTimeFormat::utc($event['start'], $bd_format)
);
if (!empty($event['finish'])) {
$event_end = day_translate(
$event_end = L10n::getDay(
!empty($event['adjust']) ?
DateTimeFormat::local($event['finish'], $bd_format) : DateTimeFormat::utc($event['finish'], $bd_format)
);
@ -49,12 +48,14 @@ class Event extends BaseObject
}
if ($simple) {
$o = '';
if (!empty($event['summary'])) {
$o = "<h3>" . BBCode::convert($event['summary'], false, $simple) . "</h3>";
$o .= "<h3>" . BBCode::convert(Strings::escapeHtml($event['summary']), false, $simple) . "</h3>";
}
if (!empty($event['desc'])) {
$o .= "<div>" . BBCode::convert($event['desc'], false, $simple) . "</div>";
$o .= "<div>" . BBCode::convert(Strings::escapeHtml($event['desc']), false, $simple) . "</div>";
}
$o .= "<h4>" . L10n::t('Starts:') . "</h4><p>" . $event_start . "</p>";
@ -64,7 +65,7 @@ class Event extends BaseObject
}
if (!empty($event['location'])) {
$o .= "<h4>" . L10n::t('Location:') . "</h4><p>" . BBCode::convert($event['location'], false, $simple) . "</p>";
$o .= "<h4>" . L10n::t('Location:') . "</h4><p>" . BBCode::convert(Strings::escapeHtml($event['location']), false, $simple) . "</p>";
}
return $o;
@ -72,7 +73,7 @@ class Event extends BaseObject
$o = '<div class="vevent">' . "\r\n";
$o .= '<div class="summary event-summary">' . BBCode::convert($event['summary'], false, $simple) . '</div>' . "\r\n";
$o .= '<div class="summary event-summary">' . BBCode::convert(Strings::escapeHtml($event['summary']), false, $simple) . '</div>' . "\r\n";
$o .= '<div class="event-start"><span class="event-label">' . L10n::t('Starts:') . '</span>&nbsp;<span class="dtstart" title="'
. DateTimeFormat::utc($event['start'], (!empty($event['adjust']) ? DateTimeFormat::ATOM : 'Y-m-d\TH:i:s'))
@ -87,12 +88,12 @@ class Event extends BaseObject
}
if (!empty($event['desc'])) {
$o .= '<div class="description event-description">' . BBCode::convert($event['desc'], false, $simple) . '</div>' . "\r\n";
$o .= '<div class="description event-description">' . BBCode::convert(Strings::escapeHtml($event['desc']), false, $simple) . '</div>' . "\r\n";
}
if (!empty($event['location'])) {
$o .= '<div class="event-location"><span class="event-label">' . L10n::t('Location:') . '</span>&nbsp;<span class="location">'
. BBCode::convert($event['location'], false, $simple)
. BBCode::convert(Strings::escapeHtml($event['location']), false, $simple)
. '</span></div>' . "\r\n";
// Include a map of the location if the [map] BBCode is used.
@ -149,6 +150,7 @@ class Event extends BaseObject
* @brief Extract bbcode formatted event data from a string.
*
* @params: string $s The string which should be parsed for event data.
* @param $text
* @return array The array with the event information.
*/
public static function fromBBCode($text)
@ -216,6 +218,7 @@ class Event extends BaseObject
*
* @param int $event_id Event ID.
* @return void
* @throws \Exception
*/
public static function delete($event_id)
{
@ -223,8 +226,8 @@ class Event extends BaseObject
return;
}
DBA::delete('event', ['id' => $event_id]);
logger("Deleted event ".$event_id, LOGGER_DEBUG);
DBA::delete('event', ['id' => $event_id], ['cascade' => false]);
Logger::log("Deleted event ".$event_id, Logger::DEBUG);
}
/**
@ -234,16 +237,16 @@ class Event extends BaseObject
*
* @param array $arr Array with event data.
* @return int The new event id.
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function store($arr)
{
$a = self::getApp();
$event = [];
$event['id'] = intval(defaults($arr, 'id' , 0));
$event['uid'] = intval(defaults($arr, 'uid' , 0));
$event['cid'] = intval(defaults($arr, 'cid' , 0));
$event['uri'] = defaults($arr, 'uri' , Item::newURI($event['uid']));
$event['guid'] = defaults($arr, 'guid' , System::createUUID());
$event['uri'] = defaults($arr, 'uri' , Item::newURI($event['uid'], $event['guid']));
$event['type'] = defaults($arr, 'type' , 'event');
$event['summary'] = defaults($arr, 'summary' , '');
$event['desc'] = defaults($arr, 'desc' , '');
@ -300,8 +303,8 @@ class Event extends BaseObject
$item = Item::selectFirst(['id'], ['event-id' => $event['id'], 'uid' => $event['uid']]);
if (DBA::isResult($item)) {
$object = '<object><type>' . xmlify(ACTIVITY_OBJ_EVENT) . '</type><title></title><id>' . xmlify($event['uri']) . '</id>';
$object .= '<content>' . xmlify(self::getBBCode($event)) . '</content>';
$object = '<object><type>' . XML::escape(ACTIVITY_OBJ_EVENT) . '</type><title></title><id>' . XML::escape($event['uri']) . '</id>';
$object .= '<content>' . XML::escape(self::getBBCode($event)) . '</content>';
$object .= '</object>' . "\n";
$fields = ['body' => self::getBBCode($event), 'object' => $object, 'edited' => $event['edited']];
@ -312,52 +315,55 @@ class Event extends BaseObject
$item_id = 0;
}
Addon::callHooks('event_updated', $event['id']);
Hook::callAll('event_updated', $event['id']);
} else {
$event['guid'] = defaults($arr, 'guid', System::createUUID());
// New event. Store it.
DBA::insert('event', $event);
$event['id'] = DBA::lastInsertId();
$item_id = 0;
$item_arr = [];
// Don't create an item for birthday events
if ($event['type'] == 'event') {
$event['id'] = DBA::lastInsertId();
$item_arr['uid'] = $event['uid'];
$item_arr['contact-id'] = $event['cid'];
$item_arr['uri'] = $event['uri'];
$item_arr['parent-uri'] = $event['uri'];
$item_arr['guid'] = $event['guid'];
$item_arr['plink'] = defaults($arr, 'plink', '');
$item_arr['post-type'] = Item::PT_EVENT;
$item_arr['wall'] = $event['cid'] ? 0 : 1;
$item_arr['contact-id'] = $contact['id'];
$item_arr['owner-name'] = $contact['name'];
$item_arr['owner-link'] = $contact['url'];
$item_arr['owner-avatar'] = $contact['thumb'];
$item_arr['author-name'] = $contact['name'];
$item_arr['author-link'] = $contact['url'];
$item_arr['author-avatar'] = $contact['thumb'];
$item_arr['title'] = '';
$item_arr['allow_cid'] = $event['allow_cid'];
$item_arr['allow_gid'] = $event['allow_gid'];
$item_arr['deny_cid'] = $event['deny_cid'];
$item_arr['deny_gid'] = $event['deny_gid'];
$item_arr['private'] = $private;
$item_arr['visible'] = 1;
$item_arr['verb'] = ACTIVITY_POST;
$item_arr['object-type'] = ACTIVITY_OBJ_EVENT;
$item_arr['origin'] = $event['cid'] === 0 ? 1 : 0;
$item_arr['body'] = self::getBBCode($event);
$item_arr['event-id'] = $event['id'];
$item_arr = [];
$item_arr['object'] = '<object><type>' . xmlify(ACTIVITY_OBJ_EVENT) . '</type><title></title><id>' . xmlify($event['uri']) . '</id>';
$item_arr['object'] .= '<content>' . xmlify(self::getBBCode($event)) . '</content>';
$item_arr['object'] .= '</object>' . "\n";
$item_arr['uid'] = $event['uid'];
$item_arr['contact-id'] = $event['cid'];
$item_arr['uri'] = $event['uri'];
$item_arr['parent-uri'] = $event['uri'];
$item_arr['guid'] = $event['guid'];
$item_arr['plink'] = defaults($arr, 'plink', '');
$item_arr['post-type'] = Item::PT_EVENT;
$item_arr['wall'] = $event['cid'] ? 0 : 1;
$item_arr['contact-id'] = $contact['id'];
$item_arr['owner-name'] = $contact['name'];
$item_arr['owner-link'] = $contact['url'];
$item_arr['owner-avatar'] = $contact['thumb'];
$item_arr['author-name'] = $contact['name'];
$item_arr['author-link'] = $contact['url'];
$item_arr['author-avatar'] = $contact['thumb'];
$item_arr['title'] = '';
$item_arr['allow_cid'] = $event['allow_cid'];
$item_arr['allow_gid'] = $event['allow_gid'];
$item_arr['deny_cid'] = $event['deny_cid'];
$item_arr['deny_gid'] = $event['deny_gid'];
$item_arr['private'] = $private;
$item_arr['visible'] = 1;
$item_arr['verb'] = ACTIVITY_POST;
$item_arr['object-type'] = ACTIVITY_OBJ_EVENT;
$item_arr['origin'] = $event['cid'] === 0 ? 1 : 0;
$item_arr['body'] = self::getBBCode($event);
$item_arr['event-id'] = $event['id'];
$item_id = Item::insert($item_arr);
$item_arr['object'] = '<object><type>' . XML::escape(ACTIVITY_OBJ_EVENT) . '</type><title></title><id>' . XML::escape($event['uri']) . '</id>';
$item_arr['object'] .= '<content>' . XML::escape(self::getBBCode($event)) . '</content>';
$item_arr['object'] .= '</object>' . "\n";
Addon::callHooks("event_created", $event['id']);
$item_id = Item::insert($item_arr);
}
Hook::callAll("event_created", $event['id']);
}
return $item_id;
@ -367,6 +373,7 @@ class Event extends BaseObject
* @brief Create an array with translation strings used for events.
*
* @return array Array with translations strings.
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getStrings()
{
@ -410,7 +417,6 @@ class Event extends BaseObject
"February" => L10n::t("February"),
"March" => L10n::t("March"),
"April" => L10n::t("April"),
"May" => L10n::t("May"),
"June" => L10n::t("June"),
"July" => L10n::t("July"),
"August" => L10n::t("August"),
@ -463,6 +469,7 @@ class Event extends BaseObject
* @param int $event_id The ID of the event in the event table
* @param string $sql_extra
* @return array Query result
* @throws \Exception
*/
public static function getListById($owner_uid, $event_id, $sql_extra = '')
{
@ -491,17 +498,18 @@ class Event extends BaseObject
/**
* @brief Get all events in a specific time frame.
*
* @param int $owner_uid The User ID of the owner of the events.
* @param array $event_params An associative array with
* int 'ignore' =>
* string 'start' => Start time of the timeframe.
* string 'finish' => Finish time of the timeframe.
* string 'adjust_start' =>
* string 'adjust_finish' =>
* @param int $owner_uid The User ID of the owner of the events.
* @param array $event_params An associative array with
* int 'ignore' =>
* string 'start' => Start time of the timeframe.
* string 'finish' => Finish time of the timeframe.
* string 'adjust_start' =>
* string 'adjust_finish' =>
*
* @param string $sql_extra Additional sql conditions (e.g. permission request).
* @param string $sql_extra Additional sql conditions (e.g. permission request).
*
* @return array Query results.
* @throws \Exception
*/
public static function getListByDate($owner_uid, $event_params, $sql_extra = '')
{
@ -542,6 +550,8 @@ class Event extends BaseObject
*
* @param array $event_result Event query array.
* @return array Event array for the template.
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function prepareListForTemplate(array $event_result)
{
@ -561,7 +571,7 @@ class Event extends BaseObject
$start = $event['adjust'] ? DateTimeFormat::local($event['start'], 'c') : DateTimeFormat::utc($event['start'], 'c');
$j = $event['adjust'] ? DateTimeFormat::local($event['start'], 'j') : DateTimeFormat::utc($event['start'], 'j');
$day = $event['adjust'] ? DateTimeFormat::local($event['start'], $fmt) : DateTimeFormat::utc($event['start'], $fmt);
$day = day_translate($day);
$day = L10n::getDay($day);
if ($event['nofinish']) {
$end = null;
@ -584,10 +594,9 @@ class Event extends BaseObject
$drop = [System::baseUrl() . '/events/drop/' . $event['id'] , L10n::t('Delete event') , '', ''];
}
$title = strip_tags(html_entity_decode(BBCode::convert($event['summary']), ENT_QUOTES, 'UTF-8'));
$title = BBCode::convert(Strings::escapeHtml($event['summary']));
if (!$title) {
list($title, $_trash) = explode("<br", BBCode::convert($event['desc']), 2);
$title = strip_tags(html_entity_decode($title, ENT_QUOTES, 'UTF-8'));
list($title, $_trash) = explode("<br", BBCode::convert(Strings::escapeHtml($event['desc'])), 2);
}
$author_link = $event['author-link'];
@ -597,8 +606,9 @@ class Event extends BaseObject
$event['plink'] = Contact::magicLink($author_link, $plink);
$html = self::getHTML($event);
$event['desc'] = BBCode::convert($event['desc']);
$event['location'] = BBCode::convert($event['location']);
$event['summary'] = BBCode::convert(Strings::escapeHtml($event['summary']));
$event['desc'] = BBCode::convert(Strings::escapeHtml($event['desc']));
$event['location'] = BBCode::convert(Strings::escapeHtml($event['location']));
$event_list[] = [
'id' => $event['id'],
'start' => $start,
@ -623,25 +633,27 @@ class Event extends BaseObject
/**
* @brief Format event to export format (ical/csv).
*
* @param array $events Query result for events.
* @param string $format The output format (ical/csv).
* @param string $timezone The timezone of the user (not implemented yet).
* @param array $events Query result for events.
* @param string $format The output format (ical/csv).
*
* @param $timezone
* @return string Content according to selected export format.
*
* @todo Implement timezone support
* @todo Implement timezone support
*/
private static function formatListForExport(array $events, $format, $timezone)
private static function formatListForExport(array $events, $format)
{
$o = '';
if (!count($events)) {
return '';
return $o;
}
switch ($format) {
// Format the exported data as a CSV file.
case "csv":
header("Content-type: text/csv");
$o = '"Subject", "Start Date", "Start Time", "Description", "End Date", "End Time", "Location"' . PHP_EOL;
$o .= '"Subject", "Start Date", "Start Time", "Description", "End Date", "End Time", "Location"' . PHP_EOL;
foreach ($events as $event) {
/// @todo The time / date entries don't include any information about the
@ -737,6 +749,7 @@ class Event extends BaseObject
* @param int $uid The user ID.
*
* @return array Query results.
* @throws \Exception
*/
private static function getListByUserId($uid = 0)
{
@ -767,33 +780,29 @@ class Event extends BaseObject
/**
*
* @param int $uid The user ID.
* @param int $uid The user ID.
* @param string $format Output format (ical/csv).
* @return array With the results:
* bool 'success' => True if the processing was successful,<br>
* string 'format' => The output format,<br>
* string 'extension' => The file extension of the output format,<br>
* string 'content' => The formatted output content.<br>
* bool 'success' => True if the processing was successful,<br>
* string 'format' => The output format,<br>
* string 'extension' => The file extension of the output format,<br>
* string 'content' => The formatted output content.<br>
*
* @throws \Exception
* @todo Respect authenticated users with events_by_uid().
*/
public static function exportListByUserId($uid, $format = 'ical')
{
$process = false;
$user = DBA::selectFirst('user', ['timezone'], ['uid' => $uid]);
if (DBA::isResult($user)) {
$timezone = $user['timezone'];
}
// Get all events which are owned by a uid (respects permissions).
$events = self::getListByUserId($uid);
// We have the events that are available for the requestor.
// Now format the output according to the requested format.
$res = self::formatListForExport($events, $format, $timezone);
$res = self::formatListForExport($events, $format);
// If there are results the precess was successfull.
// If there are results the precess was successful.
if (!empty($res)) {
$process = true;
}
@ -827,6 +836,8 @@ class Event extends BaseObject
*
* @param array $item Array with item and event data.
* @return string HTML output.
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function getItemHTML(array $item) {
$same_date = false;
@ -838,14 +849,14 @@ class Event extends BaseObject
$tformat = L10n::t('g:i A'); // 8:01 AM.
// Convert the time to different formats.
$dtstart_dt = day_translate(
$dtstart_dt = L10n::getDay(
$item['event-adjust'] ?
DateTimeFormat::local($item['event-start'], $dformat)
: DateTimeFormat::utc($item['event-start'], $dformat)
);
$dtstart_title = DateTimeFormat::utc($item['event-start'], $item['event-adjust'] ? DateTimeFormat::ATOM : 'Y-m-d\TH:i:s');
// Format: Jan till Dec.
$month_short = day_short_translate(
$month_short = L10n::getDayShort(
$item['event-adjust'] ?
DateTimeFormat::local($item['event-start'], 'M')
: DateTimeFormat::utc($item['event-start'], 'M')
@ -857,7 +868,7 @@ class Event extends BaseObject
$start_time = $item['event-adjust'] ?
DateTimeFormat::local($item['event-start'], $tformat)
: DateTimeFormat::utc($item['event-start'], $tformat);
$start_short = day_short_translate(
$start_short = L10n::getDayShort(
$item['event-adjust'] ?
DateTimeFormat::local($item['event-start'], $dformat_short)
: DateTimeFormat::utc($item['event-start'], $dformat_short)
@ -866,13 +877,13 @@ class Event extends BaseObject
// If the option 'nofinisch' isn't set, we need to format the finish date/time.
if (!$item['event-nofinish']) {
$finish = true;
$dtend_dt = day_translate(
$dtend_dt = L10n::getDay(
$item['event-adjust'] ?
DateTimeFormat::local($item['event-finish'], $dformat)
: DateTimeFormat::utc($item['event-finish'], $dformat)
);
$dtend_title = DateTimeFormat::utc($item['event-finish'], $item['event-adjust'] ? DateTimeFormat::ATOM : 'Y-m-d\TH:i:s');
$end_short = day_short_translate(
$end_short = L10n::getDayShort(
$item['event-adjust'] ?
DateTimeFormat::local($item['event-finish'], $dformat_short)
: DateTimeFormat::utc($item['event-finish'], $dformat_short)
@ -897,8 +908,8 @@ class Event extends BaseObject
// Construct the profile link (magic-auth).
$profile_link = Contact::magicLinkById($item['author-id']);
$tpl = get_markup_template('event_stream_item.tpl');
$return = replace_macros($tpl, [
$tpl = Renderer::getMarkupTemplate('event_stream_item.tpl');
$return = Renderer::replaceMacros($tpl, [
'$id' => $item['event-id'],
'$title' => prepare_text($item['event-summary']),
'$dtstart_label' => L10n::t('Starts:'),
@ -941,6 +952,7 @@ class Event extends BaseObject
* 'name' => The name of the location,<br>
* 'address' => The address of the location,<br>
* 'coordinates' => Latitude and longitude (e.g. '48.864716,2.349014').<br>
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function locationToArray($s = '') {
if ($s == '') {
@ -978,4 +990,48 @@ class Event extends BaseObject
return $location;
}
/**
* @brief Add new birthday event for this person
*
* @param array $contact Contact array, expects: id, uid, url, name
* @param string $birthday Birthday of the contact
* @return bool
* @throws \Exception
*/
public static function createBirthday($contact, $birthday)
{
// Check for duplicates
$condition = [
'uid' => $contact['uid'],
'cid' => $contact['id'],
'start' => DateTimeFormat::utc($birthday),
'type' => 'birthday'
];
if (DBA::exists('event', $condition)) {
return false;
}
/*
* Add new birthday event for this person
*
* summary is just a readable placeholder in case the event is shared
* with others. We will replace it during presentation to our $importer
* to contain a sparkle link and perhaps a photo.
*/
$values = [
'uid' => $contact['uid'],
'cid' => $contact['id'],
'start' => DateTimeFormat::utc($birthday),
'finish' => DateTimeFormat::utc($birthday . ' + 1 day '),
'summary' => L10n::t('%s\'s birthday', $contact['name']),
'desc' => L10n::t('Happy Birthday %s', ' [url=' . $contact['url'] . ']' . $contact['name'] . '[/url]'),
'type' => 'birthday',
'adjust' => 0
];
self::store($values);
return true;
}
}

320
src/Model/FileTag.php Normal file
View file

@ -0,0 +1,320 @@
<?php
/**
* @file src/Model/FileTag.php
*/
namespace Friendica\Model;
use Friendica\Core\L10n;
use Friendica\Core\PConfig;
use Friendica\Database\DBA;
/**
* @brief This class handles FileTag related functions
*
* post categories and "save to file" use the same item.file table for storage.
* We will differentiate the different uses by wrapping categories in angle brackets
* and save to file categories in square brackets.
* To do this we need to escape these characters if they appear in our tag.
*/
class FileTag
{
/**
* @brief URL encode <, >, left and right brackets
*
* @param string $s String to be URL encoded.
*
* @return string The URL encoded string.
*/
public static function encode($s)
{
return str_replace(['<', '>', '[', ']'], ['%3c', '%3e', '%5b', '%5d'], $s);
}
/**
* @brief URL decode <, >, left and right brackets
*
* @param string $s The URL encoded string to be decoded
*
* @return string The decoded string.
*/
public static function decode($s)
{
return str_replace(['%3c', '%3e', '%5b', '%5d'], ['<', '>', '[', ']'], $s);
}
/**
* @brief Query files for tag
*
* @param string $table The table to be queired.
* @param string $s The search term
* @param string $type Optional file type.
*
* @return string Query string.
*/
public static function fileQuery($table, $s, $type = 'file')
{
if ($type == 'file') {
$str = preg_quote('[' . str_replace('%', '%%', self::encode($s)) . ']');
} else {
$str = preg_quote('<' . str_replace('%', '%%', self::encode($s)) . '>');
}
return " AND " . (($table) ? DBA::escape($table) . '.' : '') . "file regexp '" . DBA::escape($str) . "' ";
}
/**
* Get file tags from array
*
* ex. given [music,video] return <music><video> or [music][video]
*
* @param array $array A list of tags.
* @param string $type Optional file type.
*
* @return string A list of file tags.
*/
public static function arrayToFile(array $array, string $type = 'file')
{
$tag_list = '';
if ($type == 'file') {
$lbracket = '[';
$rbracket = ']';
} else {
$lbracket = '<';
$rbracket = '>';
}
foreach ($array as $item) {
if (strlen($item)) {
$tag_list .= $lbracket . self::encode(trim($item)) . $rbracket;
}
}
return $tag_list;
}
/**
* Get tag list from file tags
*
* ex. given <music><video>[friends], return [music,video] or [friends]
*
* @param string $file File tags
* @param string $type Optional file type.
*
* @return array List of tag names.
*/
public static function fileToArray(string $file, string $type = 'file')
{
$matches = [];
$return = [];
if ($type == 'file') {
$cnt = preg_match_all('/\[(.*?)\]/', $file, $matches, PREG_SET_ORDER);
} else {
$cnt = preg_match_all('/<(.*?)>/', $file, $matches, PREG_SET_ORDER);
}
if ($cnt) {
foreach ($matches as $match) {
$return[] = self::decode($match[1]);
}
}
return $return;
}
/**
* @brief Get file tags from list
*
* ex. given music,video return <music><video> or [music][video]
* @param string $list A comma delimited list of tags.
* @param string $type Optional file type.
*
* @return string A list of file tags.
* @deprecated since 2019.06 use arrayToFile() instead
*/
public static function listToFile(string $list, string $type = 'file')
{
$list_array = explode(',', $list);
return self::arrayToFile($list_array, $type);
}
/**
* @brief Get list from file tags
*
* ex. given <music><video>[friends], return music,video or friends
* @param string $file File tags
* @param string $type Optional file type.
*
* @return string Comma delimited list of tag names.
* @deprecated since 2019.06 use fileToArray() instead
*/
public static function fileToList(string $file, $type = 'file')
{
return implode(',', self::fileToArray($file, $type));
}
/**
* @brief Update file tags in PConfig
*
* @param int $uid Unique Identity.
* @param string $file_old Categories previously associated with an item
* @param string $file_new New list of categories for an item
* @param string $type Optional file type.
*
* @return boolean A value indicating success or failure.
* @throws \Exception
*/
public static function updatePconfig(int $uid, string $file_old, string $file_new, string $type = 'file')
{
if (!intval($uid)) {
return false;
} elseif ($file_old == $file_new) {
return true;
}
$saved = PConfig::get($uid, 'system', 'filetags');
if (strlen($saved)) {
if ($type == 'file') {
$lbracket = '[';
$rbracket = ']';
$termtype = TERM_FILE;
} else {
$lbracket = '<';
$rbracket = '>';
$termtype = TERM_CATEGORY;
}
$filetags_updated = $saved;
// check for new tags to be added as filetags in pconfig
$new_tags = [];
foreach (self::fileToArray($file_new, $type) as $tag) {
if (!stristr($saved, $lbracket . self::encode($tag) . $rbracket)) {
$new_tags[] = $tag;
}
}
$filetags_updated .= self::arrayToFile($new_tags, $type);
// check for deleted tags to be removed from filetags in pconfig
$deleted_tags = [];
foreach (self::fileToArray($file_old, $type) as $tag) {
if (!stristr($file_new, $lbracket . self::encode($tag) . $rbracket)) {
$deleted_tags[] = $tag;
}
}
foreach ($deleted_tags as $key => $tag) {
$r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
DBA::escape($tag),
intval(Term::OBJECT_TYPE_POST),
intval($termtype),
intval($uid));
if (DBA::isResult($r)) {
unset($deleted_tags[$key]);
} else {
$filetags_updated = str_replace($lbracket . self::encode($tag) . $rbracket, '', $filetags_updated);
}
}
if ($saved != $filetags_updated) {
PConfig::set($uid, 'system', 'filetags', $filetags_updated);
}
return true;
} elseif (strlen($file_new)) {
PConfig::set($uid, 'system', 'filetags', $file_new);
}
return true;
}
/**
* @brief Add tag to file
*
* @param int $uid Unique identity.
* @param int $item_id Item identity.
* @param string $file File tag.
*
* @return boolean A value indicating success or failure.
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function saveFile($uid, $item_id, $file)
{
if (!intval($uid)) {
return false;
}
$item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
if (DBA::isResult($item)) {
if (!stristr($item['file'], '[' . self::encode($file) . ']')) {
$fields = ['file' => $item['file'] . '[' . self::encode($file) . ']'];
Item::update($fields, ['id' => $item_id]);
}
$saved = PConfig::get($uid, 'system', 'filetags');
if (!strlen($saved) || !stristr($saved, '[' . self::encode($file) . ']')) {
PConfig::set($uid, 'system', 'filetags', $saved . '[' . self::encode($file) . ']');
}
info(L10n::t('Item filed'));
}
return true;
}
/**
* @brief Remove tag from file
*
* @param int $uid Unique identity.
* @param int $item_id Item identity.
* @param string $file File tag.
* @param boolean $cat Optional value indicating the term type (i.e. Category or File)
*
* @return boolean A value indicating success or failure.
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function unsaveFile($uid, $item_id, $file, $cat = false)
{
if (!intval($uid)) {
return false;
}
if ($cat == true) {
$pattern = '<' . self::encode($file) . '>';
$termtype = Term::CATEGORY;
} else {
$pattern = '[' . self::encode($file) . ']';
$termtype = Term::FILE;
}
$item = Item::selectFirst(['file'], ['id' => $item_id, 'uid' => $uid]);
if (!DBA::isResult($item)) {
return false;
}
$fields = ['file' => str_replace($pattern, '', $item['file'])];
Item::update($fields, ['id' => $item_id]);
$r = q("SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d",
DBA::escape($file),
intval(Term::OBJECT_TYPE_POST),
intval($termtype),
intval($uid)
);
if (!DBA::isResult($r)) {
$saved = PConfig::get($uid, 'system', 'filetags');
PConfig::set($uid, 'system', 'filetags', str_replace($pattern, '', $saved));
}
return true;
}
}

View file

@ -6,18 +6,21 @@
*/
namespace Friendica\Model;
use DOMDocument;
use DOMXPath;
use Exception;
use Friendica\Core\Config;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\Network\Probe;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\PortableContact;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network;
require_once 'include/dba.php';
use Friendica\Util\Strings;
/**
* @brief This class handles GlobalContact related functions
@ -31,6 +34,7 @@ class GContact
* @param string $mode Search mode (e.g. "community")
*
* @return array with search results
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function searchByName($search, $mode = '')
{
@ -91,6 +95,7 @@ class GContact
* @param integer $cid Contact ID
* @param integer $zcid Global Contact ID
* @return void
* @throws Exception
*/
public static function link($gcid, $uid = 0, $cid = 0, $zcid = 0)
{
@ -105,16 +110,16 @@ class GContact
/**
* @brief Sanitize the given gcontact data
*
* @param array $gcontact array with gcontact data
* @throw Exception
*
* Generation:
* 0: No definition
* 1: Profiles on this server
* 2: Contacts of profiles on this server
* 3: Contacts of contacts of profiles on this server
* 4: ...
*
* @param array $gcontact array with gcontact data
* @return array $gcontact
* @throws Exception
*/
public static function sanitize($gcontact)
{
@ -140,14 +145,14 @@ class GContact
}
// Assure that there are no parameter fragments in the profile url
if (in_array($gcontact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ''])) {
if (empty($gcontact['*network']) || in_array($gcontact["network"], Protocol::FEDERATED)) {
$gcontact['url'] = self::cleanContactUrl($gcontact['url']);
}
$alternate = PortableContact::alternateOStatusUrl($gcontact['url']);
// The global contacts should contain the original picture, not the cached one
if (($gcontact['generation'] != 1) && stristr(normalise_link($gcontact['photo']), normalise_link(System::baseUrl().'/photo/'))) {
if (($gcontact['generation'] != 1) && stristr(Strings::normaliseLink($gcontact['photo']), Strings::normaliseLink(System::baseUrl() . '/photo/'))) {
$gcontact['photo'] = '';
}
@ -155,7 +160,7 @@ class GContact
$gcontact['network'] = '';
$condition = ["`uid` = 0 AND `nurl` = ? AND `network` != '' AND `network` != ?",
normalise_link($gcontact['url']), Protocol::STATUSNET];
Strings::normaliseLink($gcontact['url']), Protocol::STATUSNET];
$contact = DBA::selectFirst('contact', ['network'], $condition);
if (DBA::isResult($contact)) {
$gcontact['network'] = $contact['network'];
@ -163,7 +168,7 @@ class GContact
if (($gcontact['network'] == '') || ($gcontact['network'] == Protocol::OSTATUS)) {
$condition = ["`uid` = 0 AND `alias` IN (?, ?) AND `network` != '' AND `network` != ?",
$gcontact['url'], normalise_link($gcontact['url']), Protocol::STATUSNET];
$gcontact['url'], Strings::normaliseLink($gcontact['url']), Protocol::STATUSNET];
$contact = DBA::selectFirst('contact', ['network'], $condition);
if (DBA::isResult($contact)) {
$gcontact['network'] = $contact['network'];
@ -172,7 +177,7 @@ class GContact
}
$fields = ['network', 'updated', 'server_url', 'url', 'addr'];
$gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => normalise_link($gcontact['url'])]);
$gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => Strings::normaliseLink($gcontact['url'])]);
if (DBA::isResult($gcnt)) {
if (!isset($gcontact['network']) && ($gcnt['network'] != Protocol::STATUSNET)) {
$gcontact['network'] = $gcnt['network'];
@ -180,7 +185,7 @@ class GContact
if ($gcontact['updated'] <= DBA::NULL_DATETIME) {
$gcontact['updated'] = $gcnt['updated'];
}
if (!isset($gcontact['server_url']) && (normalise_link($gcnt['server_url']) != normalise_link($gcnt['url']))) {
if (!isset($gcontact['server_url']) && (Strings::normaliseLink($gcnt['server_url']) != Strings::normaliseLink($gcnt['url']))) {
$gcontact['server_url'] = $gcnt['server_url'];
}
if (!isset($gcontact['addr'])) {
@ -189,7 +194,7 @@ class GContact
}
if ((!isset($gcontact['network']) || !isset($gcontact['name']) || !isset($gcontact['addr']) || !isset($gcontact['photo']) || !isset($gcontact['server_url']) || $alternate)
&& PortableContact::reachable($gcontact['url'], $gcontact['server_url'], $gcontact['network'], false)
&& GServer::reachable($gcontact['url'], $gcontact['server_url'], $gcontact['network'], false)
) {
$data = Probe::uri($gcontact['url']);
@ -205,8 +210,8 @@ class GContact
if ($alternate && ($gcontact['network'] == Protocol::OSTATUS)) {
// Delete the old entry - if it exists
if (DBA::exists('gcontact', ['nurl' => normalise_link($orig_profile)])) {
DBA::delete('gcontact', ['nurl' => normalise_link($orig_profile)]);
if (DBA::exists('gcontact', ['nurl' => Strings::normaliseLink($orig_profile)])) {
DBA::delete('gcontact', ['nurl' => Strings::normaliseLink($orig_profile)]);
}
}
}
@ -215,13 +220,13 @@ class GContact
throw new Exception('No name and photo for URL '.$gcontact['url']);
}
if (!in_array($gcontact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
if (!in_array($gcontact['network'], Protocol::FEDERATED)) {
throw new Exception('No federated network ('.$gcontact['network'].') detected for URL '.$gcontact['url']);
}
if (empty($gcontact['server_url'])) {
// We check the server url to be sure that it is a real one
$server_url = PortableContact::detectServer($gcontact['url']);
$server_url = Contact::getBasepath($gcontact['url']);
// We are now sure that it is a correct URL. So we use it in the future
if ($server_url != '') {
@ -230,7 +235,7 @@ class GContact
}
// The server URL doesn't seem to be valid, so we don't store it.
if (!PortableContact::checkServer($gcontact['server_url'], $gcontact['network'])) {
if (!GServer::check($gcontact['server_url'], $gcontact['network'])) {
$gcontact['server_url'] = '';
}
@ -241,6 +246,7 @@ class GContact
* @param integer $uid id
* @param integer $cid id
* @return integer
* @throws Exception
*/
public static function countCommonFriends($uid, $cid)
{
@ -257,7 +263,7 @@ class GContact
intval($cid)
);
// logger("countCommonFriends: $uid $cid {$r[0]['total']}");
// Logger::log("countCommonFriends: $uid $cid {$r[0]['total']}");
if (DBA::isResult($r)) {
return $r[0]['total'];
}
@ -268,6 +274,7 @@ class GContact
* @param integer $uid id
* @param integer $zcid zcid
* @return integer
* @throws Exception
*/
public static function countCommonFriendsZcid($uid, $zcid)
{
@ -294,6 +301,7 @@ class GContact
* @param integer $limit optional, default 9999
* @param boolean $shuffle optional, default false
* @return object
* @throws Exception
*/
public static function commonFriends($uid, $cid, $start = 0, $limit = 9999, $shuffle = false)
{
@ -332,6 +340,7 @@ class GContact
* @param integer $limit optional, default 9999
* @param boolean $shuffle optional, default false
* @return object
* @throws Exception
*/
public static function commonFriendsZcid($uid, $zcid, $start = 0, $limit = 9999, $shuffle = false)
{
@ -361,6 +370,7 @@ class GContact
* @param integer $uid user
* @param integer $cid cid
* @return integer
* @throws Exception
*/
public static function countAllFriends($uid, $cid)
{
@ -386,6 +396,7 @@ class GContact
* @param integer $start optional, default 0
* @param integer $limit optional, default 80
* @return array
* @throws Exception
*/
public static function allFriends($uid, $cid, $start = 0, $limit = 80)
{
@ -409,10 +420,11 @@ class GContact
}
/**
* @param object $uid user
* @param int $uid user
* @param integer $start optional, default 0
* @param integer $limit optional, default 80
* @return array
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function suggestionQuery($uid, $start = 0, $limit = 80)
{
@ -429,7 +441,7 @@ class GContact
// return $list;
//}
$network = [Protocol::DFRN];
$network = [Protocol::DFRN, Protocol::ACTIVITYPUB];
if (Config::get('system', 'diaspora_enabled')) {
$network[] = Protocol::DIASPORA;
@ -450,7 +462,7 @@ class GContact
where uid = %d and not gcontact.nurl in ( select nurl from contact where uid = %d )
AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
AND `gcontact`.`updated` >= '%s'
AND `gcontact`.`updated` >= '%s' AND NOT `gcontact`.`hide`
AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
AND `gcontact`.`network` IN (%s)
GROUP BY `glink`.`gcid` ORDER BY `gcontact`.`updated` DESC,`total` DESC LIMIT %d, %d",
@ -516,11 +528,10 @@ class GContact
/**
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function updateSuggestions()
{
$a = get_app();
$done = [];
/// @TODO Check if it is really neccessary to poll the own server
@ -534,7 +545,7 @@ class GContact
$j = json_decode($x);
if (!empty($j->entries)) {
foreach ($j->entries as $entry) {
PortableContact::checkServer($entry->url);
GServer::check($entry->url);
$url = $entry->url . '/poco';
if (!in_array($url, $done)) {
@ -569,6 +580,7 @@ class GContact
* @param string $url Contact url
*
* @return string Contact url with the wanted parts
* @throws Exception
*/
public static function cleanContactUrl($url)
{
@ -589,7 +601,7 @@ class GContact
}
if ($new_url != $url) {
logger("Cleaned contact url ".$url." to ".$new_url." - Called by: ".System::callstack(), LOGGER_DEBUG);
Logger::log("Cleaned contact url ".$url." to ".$new_url." - Called by: ".System::callstack(), Logger::DEBUG);
}
return $new_url;
@ -600,13 +612,15 @@ class GContact
*
* @param array $contact contact array (called by reference)
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function fixAlternateContactAddress(&$contact)
{
if (($contact["network"] == Protocol::OSTATUS) && PortableContact::alternateOStatusUrl($contact["url"])) {
$data = Probe::uri($contact["url"]);
if ($contact["network"] == Protocol::OSTATUS) {
logger("Fix primary url from ".$contact["url"]." to ".$data["url"]." - Called by: ".System::callstack(), LOGGER_DEBUG);
Logger::log("Fix primary url from ".$contact["url"]." to ".$data["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
$contact["url"] = $data["url"];
$contact["addr"] = $data["addr"];
$contact["alias"] = $data["alias"];
@ -621,6 +635,8 @@ class GContact
* @param array $contact contact array
*
* @return bool|int Returns false if not found, integer if contact was found
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function getId($contact)
{
@ -630,12 +646,12 @@ class GContact
$last_contact_str = '';
if (empty($contact["network"])) {
logger("Empty network for contact url ".$contact["url"]." - Called by: ".System::callstack(), LOGGER_DEBUG);
Logger::log("Empty network for contact url ".$contact["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
return false;
}
if (in_array($contact["network"], [Protocol::PHANTOM])) {
logger("Invalid network for contact url ".$contact["url"]." - Called by: ".System::callstack(), LOGGER_DEBUG);
Logger::log("Invalid network for contact url ".$contact["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
return false;
}
@ -652,13 +668,13 @@ class GContact
self::fixAlternateContactAddress($contact);
// Remove unwanted parts from the contact url (e.g. "?zrl=...")
if (in_array($contact["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
if (in_array($contact["network"], Protocol::FEDERATED)) {
$contact["url"] = self::cleanContactUrl($contact["url"]);
}
DBA::lock('gcontact');
$fields = ['id', 'last_contact', 'last_failure', 'network'];
$gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => normalise_link($contact["url"])]);
$gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => Strings::normaliseLink($contact["url"])]);
if (DBA::isResult($gcnt)) {
$gcontact_id = $gcnt["id"];
@ -683,7 +699,7 @@ class GContact
DBA::escape($contact["addr"]),
DBA::escape($contact["network"]),
DBA::escape($contact["url"]),
DBA::escape(normalise_link($contact["url"])),
DBA::escape(Strings::normaliseLink($contact["url"])),
DBA::escape($contact["photo"]),
DBA::escape(DateTimeFormat::utcNow()),
DBA::escape(DateTimeFormat::utcNow()),
@ -693,7 +709,7 @@ class GContact
intval($contact["generation"])
);
$condition = ['nurl' => normalise_link($contact["url"])];
$condition = ['nurl' => Strings::normaliseLink($contact["url"])];
$cnt = DBA::selectFirst('gcontact', ['id', 'network'], $condition, ['order' => ['id']]);
if (DBA::isResult($cnt)) {
$gcontact_id = $cnt["id"];
@ -703,7 +719,7 @@ class GContact
DBA::unlock();
if ($doprobing) {
logger("Last Contact: ". $last_contact_str." - Last Failure: ".$last_failure_str." - Checking: ".$contact["url"], LOGGER_DEBUG);
Logger::log("Last Contact: ". $last_contact_str." - Last Failure: ".$last_failure_str." - Checking: ".$contact["url"], Logger::DEBUG);
Worker::add(PRIORITY_LOW, 'GProbe', $contact["url"]);
}
@ -716,6 +732,8 @@ class GContact
* @param array $contact contact array
*
* @return bool|int Returns false if not found, integer if contact was found
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function update($contact)
{
@ -732,174 +750,434 @@ class GContact
return false;
}
$public_contact = q(
"SELECT `name`, `nick`, `photo`, `location`, `about`, `addr`, `generation`, `birthday`, `gender`, `keywords`,
`contact-type`, `hide`, `nsfw`, `network`, `alias`, `notify`, `server_url`, `connect`, `updated`, `url`
FROM `gcontact` WHERE `id` = %d LIMIT 1",
intval($gcontact_id)
);
$public_contact = DBA::selectFirst('gcontact', [
'name', 'nick', 'photo', 'location', 'about', 'addr', 'generation', 'birthday', 'gender', 'keywords',
'contact-type', 'hide', 'nsfw', 'network', 'alias', 'notify', 'server_url', 'connect', 'updated', 'url'
], ['id' => $gcontact_id]);
if (!DBA::isResult($public_contact)) {
return false;
}
// Get all field names
$fields = [];
foreach ($public_contact[0] as $field => $data) {
foreach ($public_contact as $field => $data) {
$fields[$field] = $data;
}
unset($fields["url"]);
unset($fields["updated"]);
unset($fields["hide"]);
unset($fields['url']);
unset($fields['updated']);
unset($fields['hide']);
// Bugfix: We had an error in the storing of keywords which lead to the "0"
// This value is still transmitted via poco.
if (!empty($contact["keywords"]) && ($contact["keywords"] == "0")) {
unset($contact["keywords"]);
if (isset($contact['keywords']) && ($contact['keywords'] == '0')) {
unset($contact['keywords']);
}
if (!empty($public_contact[0]["keywords"]) && ($public_contact[0]["keywords"] == "0")) {
$public_contact[0]["keywords"] = "";
if (isset($public_contact['keywords']) && ($public_contact['keywords'] == '0')) {
$public_contact['keywords'] = '';
}
// assign all unassigned fields from the database entry
foreach ($fields as $field => $data) {
if (!isset($contact[$field]) || ($contact[$field] == "")) {
$contact[$field] = $public_contact[0][$field];
if (empty($contact[$field])) {
$contact[$field] = $public_contact[$field];
}
}
if (!isset($contact["hide"])) {
$contact["hide"] = $public_contact[0]["hide"];
if (!isset($contact['hide'])) {
$contact['hide'] = $public_contact['hide'];
}
$fields["hide"] = $public_contact[0]["hide"];
$fields['hide'] = $public_contact['hide'];
if ($contact["network"] == Protocol::STATUSNET) {
$contact["network"] = Protocol::OSTATUS;
if ($contact['network'] == Protocol::STATUSNET) {
$contact['network'] = Protocol::OSTATUS;
}
// Replace alternate OStatus user format with the primary one
self::fixAlternateContactAddress($contact);
if (!isset($contact["updated"])) {
$contact["updated"] = DateTimeFormat::utcNow();
if (!isset($contact['updated'])) {
$contact['updated'] = DateTimeFormat::utcNow();
}
if ($contact["network"] == Protocol::TWITTER) {
$contact["server_url"] = 'http://twitter.com';
if ($contact['network'] == Protocol::TWITTER) {
$contact['server_url'] = 'http://twitter.com';
}
if ($contact["server_url"] == "") {
$data = Probe::uri($contact["url"]);
if ($data["network"] != Protocol::PHANTOM) {
$contact["server_url"] = $data['baseurl'];
if (empty($contact['server_url'])) {
$data = Probe::uri($contact['url']);
if ($data['network'] != Protocol::PHANTOM) {
$contact['server_url'] = $data['baseurl'];
}
} else {
$contact["server_url"] = normalise_link($contact["server_url"]);
$contact['server_url'] = Strings::normaliseLink($contact['server_url']);
}
if (($contact["addr"] == "") && ($contact["server_url"] != "") && ($contact["nick"] != "")) {
$hostname = str_replace("http://", "", $contact["server_url"]);
$contact["addr"] = $contact["nick"]."@".$hostname;
if (empty($contact['addr']) && !empty($contact['server_url']) && !empty($contact['nick'])) {
$hostname = str_replace('http://', '', $contact['server_url']);
$contact['addr'] = $contact['nick'] . '@' . $hostname;
}
// Check if any field changed
$update = false;
unset($fields["generation"]);
unset($fields['generation']);
if ((($contact["generation"] > 0) && ($contact["generation"] <= $public_contact[0]["generation"])) || ($public_contact[0]["generation"] == 0)) {
if ((($contact['generation'] > 0) && ($contact['generation'] <= $public_contact['generation'])) || ($public_contact['generation'] == 0)) {
foreach ($fields as $field => $data) {
if ($contact[$field] != $public_contact[0][$field]) {
logger("Difference for contact ".$contact["url"]." in field '".$field."'. New value: '".$contact[$field]."', old value '".$public_contact[0][$field]."'", LOGGER_DEBUG);
if ($contact[$field] != $public_contact[$field]) {
Logger::debug('Difference found.', ['contact' => $contact["url"], 'field' => $field, 'new' => $contact[$field], 'old' => $public_contact[$field]]);
$update = true;
}
}
if ($contact["generation"] < $public_contact[0]["generation"]) {
logger("Difference for contact ".$contact["url"]." in field 'generation'. new value: '".$contact["generation"]."', old value '".$public_contact[0]["generation"]."'", LOGGER_DEBUG);
if ($contact['generation'] < $public_contact['generation']) {
Logger::debug('Difference found.', ['contact' => $contact["url"], 'field' => 'generation', 'new' => $contact['generation'], 'old' => $public_contact['generation']]);
$update = true;
}
}
if ($update) {
logger("Update gcontact for ".$contact["url"], LOGGER_DEBUG);
Logger::debug('Update gcontact.', ['contact' => $contact['url']]);
$condition = ['`nurl` = ? AND (`generation` = 0 OR `generation` >= ?)',
normalise_link($contact["url"]), $contact["generation"]];
Strings::normaliseLink($contact["url"]), $contact["generation"]];
$contact["updated"] = DateTimeFormat::utc($contact["updated"]);
$updated = ['photo' => $contact['photo'], 'name' => $contact['name'],
'nick' => $contact['nick'], 'addr' => $contact['addr'],
'network' => $contact['network'], 'birthday' => $contact['birthday'],
'gender' => $contact['gender'], 'keywords' => $contact['keywords'],
'hide' => $contact['hide'], 'nsfw' => $contact['nsfw'],
'contact-type' => $contact['contact-type'], 'alias' => $contact['alias'],
'notify' => $contact['notify'], 'url' => $contact['url'],
'location' => $contact['location'], 'about' => $contact['about'],
'generation' => $contact['generation'], 'updated' => $contact['updated'],
'server_url' => $contact['server_url'], 'connect' => $contact['connect']];
$updated = [
'photo' => $contact['photo'], 'name' => $contact['name'],
'nick' => $contact['nick'], 'addr' => $contact['addr'],
'network' => $contact['network'], 'birthday' => $contact['birthday'],
'gender' => $contact['gender'], 'keywords' => $contact['keywords'],
'hide' => $contact['hide'], 'nsfw' => $contact['nsfw'],
'contact-type' => $contact['contact-type'], 'alias' => $contact['alias'],
'notify' => $contact['notify'], 'url' => $contact['url'],
'location' => $contact['location'], 'about' => $contact['about'],
'generation' => $contact['generation'], 'updated' => $contact['updated'],
'server_url' => $contact['server_url'], 'connect' => $contact['connect']
];
DBA::update('gcontact', $updated, $condition, $fields);
// Now update the contact entry with the user id "0" as well.
// This is used for the shadow copies of public items.
/// @todo Check if we really should do this.
// The quality of the gcontact table is mostly lower than the public contact
$public_contact = DBA::selectFirst('contact', ['id'], ['nurl' => normalise_link($contact["url"]), 'uid' => 0]);
if (DBA::isResult($public_contact)) {
logger("Update public contact ".$public_contact["id"], LOGGER_DEBUG);
Contact::updateAvatar($contact["photo"], 0, $public_contact["id"]);
$fields = ['name', 'nick', 'addr',
'network', 'bd', 'gender',
'keywords', 'alias', 'contact-type',
'url', 'location', 'about'];
$old_contact = DBA::selectFirst('contact', $fields, ['id' => $public_contact["id"]]);
// Update it with the current values
$fields = ['name' => $contact['name'], 'nick' => $contact['nick'],
'addr' => $contact['addr'], 'network' => $contact['network'],
'bd' => $contact['birthday'], 'gender' => $contact['gender'],
'keywords' => $contact['keywords'], 'alias' => $contact['alias'],
'contact-type' => $contact['contact-type'], 'url' => $contact['url'],
'location' => $contact['location'], 'about' => $contact['about']];
// Don't update the birthday field if not set or invalid
if (empty($contact['birthday']) || ($contact['birthday'] < '0001-01-01')) {
unset($fields['bd']);
}
DBA::update('contact', $fields, ['id' => $public_contact["id"]], $old_contact);
}
}
return $gcontact_id;
}
/**
* Set the last date that the contact had posted something
*
* @param string $data Probing result
* @param bool $force force updating
*/
public static function setLastUpdate(array $data, bool $force = false)
{
// Fetch the global contact
$gcontact = DBA::selectFirst('gcontact', ['created', 'updated', 'last_contact', 'last_failure'],
['nurl' => Strings::normaliseLink($data['url'])]);
if (!DBA::isResult($gcontact)) {
return;
}
if (!$force && !PortableContact::updateNeeded($gcontact['created'], $gcontact['updated'], $gcontact['last_failure'], $gcontact['last_contact'])) {
Logger::info("Don't update profile", ['url' => $data['url'], 'updated' => $gcontact['updated']]);
return;
}
if (self::updateFromNoScrape($data)) {
return;
}
// When the profile doesn't have got a feed, then we exit here
if (empty($data['poll'])) {
return;
}
if ($data['network'] == Protocol::ACTIVITYPUB) {
self::updateFromOutbox($data['poll'], $data);
} else {
self::updateFromFeed($data);
}
}
/**
* Update a global contact via the "noscrape" endpoint
*
* @param string $data Probing result
*
* @return bool 'true' if update was successful or the server was unreachable
*/
private static function updateFromNoScrape(array $data)
{
// Check the 'noscrape' endpoint when it is a Friendica server
$gserver = DBA::selectFirst('gserver', ['noscrape'], ["`nurl` = ? AND `noscrape` != ''",
Strings::normaliseLink($data['baseurl'])]);
if (!DBA::isResult($gserver)) {
return false;
}
$curlResult = Network::curl($gserver['noscrape'] . '/' . $data['nick']);
if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
$noscrape = json_decode($curlResult->getBody(), true);
if (!empty($noscrape)) {
$noscrape['updated'] = DateTimeFormat::utc($noscrape['updated'], DateTimeFormat::MYSQL);
$fields = ['last_contact' => DateTimeFormat::utcNow(), 'updated' => $noscrape['updated']];
DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
return true;
}
} elseif ($curlResult->isTimeout()) {
// On a timeout return the existing value, but mark the contact as failure
$fields = ['last_failure' => DateTimeFormat::utcNow()];
DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
return true;
}
return false;
}
/**
* Update a global contact via an ActivityPub Outbox
*
* @param string $data Probing result
*/
private static function updateFromOutbox(string $feed, array $data)
{
$outbox = ActivityPub::fetchContent($feed);
if (empty($outbox)) {
return;
}
if (!empty($outbox['orderedItems'])) {
$items = $outbox['orderedItems'];
} elseif (!empty($outbox['first']['orderedItems'])) {
$items = $outbox['first']['orderedItems'];
} elseif (!empty($outbox['first'])) {
self::updateFromOutbox($outbox['first'], $data);
return;
} else {
$items = [];
}
$last_updated = '';
foreach ($items as $activity) {
if ($last_updated < $activity['published']) {
$last_updated = $activity['published'];
}
}
if (empty($last_updated)) {
return;
}
$fields = ['last_contact' => DateTimeFormat::utcNow(), 'updated' => $last_updated];
DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
}
/**
* Update a global contact via an XML feed
*
* @param string $data Probing result
*/
private static function updateFromFeed(array $data)
{
// Search for the newest entry in the feed
$curlResult = Network::curl($data['poll']);
if (!$curlResult->isSuccess()) {
$fields = ['last_failure' => DateTimeFormat::utcNow()];
DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($profile)]);
Logger::info("Profile wasn't reachable (no feed)", ['url' => $data['url']]);
return;
}
$doc = new DOMDocument();
@$doc->loadXML($curlResult->getBody());
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
$entries = $xpath->query('/atom:feed/atom:entry');
$last_updated = '';
foreach ($entries as $entry) {
$published_item = $xpath->query('atom:published/text()', $entry)->item(0);
$updated_item = $xpath->query('atom:updated/text()' , $entry)->item(0);
$published = !empty($published_item->nodeValue) ? DateTimeFormat::utc($published_item->nodeValue) : null;
$updated = !empty($updated_item->nodeValue) ? DateTimeFormat::utc($updated_item->nodeValue) : null;
if (empty($published) || empty($updated)) {
Logger::notice('Invalid entry for XPath.', ['entry' => $entry, 'url' => $data['url']]);
continue;
}
if ($last_updated < $published) {
$last_updated = $published;
}
if ($last_updated < $updated) {
$last_updated = $updated;
}
}
if (empty($last_updated)) {
return;
}
$fields = ['last_contact' => DateTimeFormat::utcNow(), 'updated' => $last_updated];
DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
}
/**
* @brief Updates the gcontact entry from a given public contact id
*
* @param integer $cid contact id
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function updateFromPublicContactID($cid)
{
self::updateFromPublicContact(['id' => $cid]);
}
/**
* @brief Updates the gcontact entry from a given public contact url
*
* @param string $url contact url
* @return integer gcontact id
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function updateFromPublicContactURL($url)
{
return self::updateFromPublicContact(['nurl' => Strings::normaliseLink($url)]);
}
/**
* @brief Helper function for updateFromPublicContactID and updateFromPublicContactURL
*
* @param array $condition contact condition
* @return integer gcontact id
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function updateFromPublicContact($condition)
{
$fields = ['name', 'nick', 'url', 'nurl', 'location', 'about', 'keywords', 'gender',
'bd', 'contact-type', 'network', 'addr', 'notify', 'alias', 'archive', 'term-date',
'created', 'updated', 'avatar', 'success_update', 'failure_update', 'forum', 'prv',
'baseurl', 'sensitive', 'unsearchable'];
$contact = DBA::selectFirst('contact', $fields, array_merge($condition, ['uid' => 0, 'network' => Protocol::FEDERATED]));
if (!DBA::isResult($contact)) {
return 0;
}
$fields = ['name', 'nick', 'url', 'nurl', 'location', 'about', 'keywords', 'gender', 'generation',
'birthday', 'contact-type', 'network', 'addr', 'notify', 'alias', 'archived', 'archive_date',
'created', 'updated', 'photo', 'last_contact', 'last_failure', 'community', 'connect',
'server_url', 'nsfw', 'hide', 'id'];
$old_gcontact = DBA::selectFirst('gcontact', $fields, ['nurl' => $contact['nurl']]);
$do_insert = !DBA::isResult($old_gcontact);
if ($do_insert) {
$old_gcontact = [];
}
$gcontact = [];
// These fields are identical in both contact and gcontact
$fields = ['name', 'nick', 'url', 'nurl', 'location', 'about', 'keywords', 'gender',
'contact-type', 'network', 'addr', 'notify', 'alias', 'created', 'updated'];
foreach ($fields as $field) {
$gcontact[$field] = $contact[$field];
}
// These fields are having different names but the same content
$gcontact['server_url'] = $contact['baseurl'] ?? ''; // "baseurl" can be null, "server_url" not
$gcontact['nsfw'] = $contact['sensitive'];
$gcontact['hide'] = $contact['unsearchable'];
$gcontact['archived'] = $contact['archive'];
$gcontact['archive_date'] = $contact['term-date'];
$gcontact['birthday'] = $contact['bd'];
$gcontact['photo'] = $contact['avatar'];
$gcontact['last_contact'] = $contact['success_update'];
$gcontact['last_failure'] = $contact['failure_update'];
$gcontact['community'] = ($contact['forum'] || $contact['prv']);
foreach (['last_contact', 'last_failure', 'updated'] as $field) {
if (!empty($old_gcontact[$field]) && ($old_gcontact[$field] >= $gcontact[$field])) {
unset($gcontact[$field]);
}
}
if (!$gcontact['archived']) {
$gcontact['archive_date'] = DBA::NULL_DATETIME;
}
if (!empty($old_gcontact['created']) && ($old_gcontact['created'] > DBA::NULL_DATETIME)
&& ($old_gcontact['created'] <= $gcontact['created'])) {
unset($gcontact['created']);
}
if (empty($gcontact['birthday']) && ($gcontact['birthday'] <= DBA::NULL_DATETIME)) {
unset($gcontact['birthday']);
}
if (empty($old_gcontact['generation']) || ($old_gcontact['generation'] > 2)) {
$gcontact['generation'] = 2; // We fetched the data directly from the other server
}
if (!$do_insert) {
DBA::update('gcontact', $gcontact, ['nurl' => $contact['nurl']], $old_gcontact);
return $old_gcontact['id'];
} elseif (!$gcontact['archived']) {
DBA::insert('gcontact', $gcontact);
return DBA::lastInsertId();
}
}
/**
* @brief Updates the gcontact entry from probe
*
* @param string $url profile link
* @return void
* @param string $url profile link
* @param boolean $force Optional forcing of network probing (otherwise we use the cached data)
*
* @return boolean 'true' when contact had been updated
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function updateFromProbe($url)
public static function updateFromProbe($url, $force = false)
{
$data = Probe::uri($url);
$data = Probe::uri($url, $force);
if (in_array($data["network"], [Protocol::PHANTOM])) {
logger("Invalid network for contact url ".$data["url"]." - Called by: ".System::callstack(), LOGGER_DEBUG);
return;
$fields = ['last_failure' => DateTimeFormat::utcNow()];
DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($url)]);
Logger::info('Invalid network for contact', ['url' => $data['url'], 'callstack' => System::callstack()]);
return false;
}
$data["server_url"] = $data["baseurl"];
self::update($data);
// Set the date of the latest post
self::setLastUpdate($data, $force);
return true;
}
/**
* @brief Update the gcontact entry for a given user id
*
* @param int $uid User ID
* @return void
* @return bool
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function updateForUser($uid)
{
@ -917,7 +1195,7 @@ class GContact
);
if (!DBA::isResult($r)) {
logger('Cannot find user with uid=' . $uid, LOGGER_INFO);
Logger::log('Cannot find user with uid=' . $uid, Logger::INFO);
return false;
}
@ -950,11 +1228,13 @@ class GContact
* If the "Statistics" addon is enabled (See http://gstools.org/ for details) we query user data with this.
*
* @param string $server Server address
* @return void
* @return bool
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function fetchGsUsers($server)
{
logger("Fetching users from GNU Social server ".$server, LOGGER_DEBUG);
Logger::log("Fetching users from GNU Social server ".$server, Logger::DEBUG);
$url = $server."/main/statistics";
@ -965,8 +1245,8 @@ class GContact
$statistics = json_decode($curlResult->getBody());
if (!empty($statistics->config)) {
if ($statistics->config->instance_with_ssl) {
if (!empty($statistics->config->instance_address)) {
if (!empty($statistics->config->instance_with_ssl)) {
$server = "https://";
} else {
$server = "http://";
@ -975,8 +1255,8 @@ class GContact
$server .= $statistics->config->instance_address;
$hostname = $statistics->config->instance_address;
} elseif (!empty($statistics)) {
if ($statistics->instance_with_ssl) {
} elseif (!empty($statistics->instance_address)) {
if (!empty($statistics->instance_with_ssl)) {
$server = "https://";
} else {
$server = "http://";
@ -1010,6 +1290,8 @@ class GContact
/**
* @brief Asking GNU Social server on a regular base for their user data
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function discoverGsUsers()
{
@ -1017,11 +1299,16 @@ class GContact
$last_update = date("c", time() - (60 * 60 * 24 * $requery_days));
$r = q(
"SELECT `nurl`, `url` FROM `gserver` WHERE `last_contact` >= `last_failure` AND `network` = '%s' AND `last_poco_query` < '%s' ORDER BY RAND() LIMIT 5",
DBA::escape(Protocol::OSTATUS),
DBA::escape($last_update)
);
$r = DBA::select('gserver', ['nurl', 'url'], [
'`network` = ?
AND `last_contact` >= `last_failure`
AND `last_poco_query` < ?',
Protocol::OSTATUS,
$last_update
], [
'limit' => 5,
'order' => ['RAND()']
]);
if (!DBA::isResult($r)) {
return;
@ -1034,20 +1321,23 @@ class GContact
}
/**
* @return string
* Returns a random, global contact of the current node
*
* @return string The profile URL
* @throws Exception
*/
public static function getRandomUrl()
{
$r = q(
"SELECT `url` FROM `gcontact` WHERE `network` = '%s'
AND `last_contact` >= `last_failure`
AND `updated` > UTC_TIMESTAMP - INTERVAL 1 MONTH
ORDER BY rand() LIMIT 1",
DBA::escape(Protocol::DFRN)
);
$r = DBA::selectFirst('gcontact', ['url'], [
'`network` = ?
AND `last_contact` >= `last_failure`
AND `updated` > ?',
Protocol::DFRN,
DateTimeFormat::utc('now - 1 month'),
], ['order' => ['RAND()']]);
if (DBA::isResult($r)) {
return dirname($r[0]['url']);
return $r['url'];
}
return '';

1187
src/Model/GServer.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -2,36 +2,68 @@
/**
* @file src/Model/Group.php
*/
namespace Friendica\Model;
use Friendica\BaseModule;
use Friendica\BaseObject;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Database\DBA;
use Friendica\Util\Security;
require_once 'boot.php';
require_once 'include/dba.php';
require_once 'include/text.php';
/**
* @brief functions for interacting with the group database table
*/
class Group extends BaseObject
{
const FOLLOWERS = '~';
const MUTUALS = '&';
public static function getByUserId($uid, $includesDeleted = false)
{
$conditions = ['uid' => $uid];
if (!$includesDeleted) {
$conditions['deleted'] = false;
}
return DBA::selectToArray('group', [], $conditions);
}
/**
* @param int $group_id
* @return bool
* @throws \Exception
*/
public static function exists($group_id, $uid = null)
{
$condition = ['id' => $group_id, 'deleted' => false];
if (isset($uid)) {
$condition = [
'uid' => $uid
];
}
return DBA::exists('group', $condition);
}
/**
* @brief Create a new contact group
*
* Note: If we found a deleted group with the same name, we restore it
*
* @param int $uid
* @param int $uid
* @param string $name
* @return boolean
* @throws \Exception
*/
public static function create($uid, $name)
{
$return = false;
if (x($uid) && x($name)) {
if (!empty($uid) && !empty($name)) {
$gid = self::getIdByName($uid, $name); // check for dupes
if ($gid !== false) {
// This could be a problem.
@ -58,10 +90,11 @@ class Group extends BaseObject
/**
* Update group information.
*
* @param int $id Group ID
* @param string $name Group name
* @param int $id Group ID
* @param string $name Group name
*
* @return bool Was the update successful?
* @throws \Exception
*/
public static function update($id, $name)
{
@ -73,17 +106,17 @@ class Group extends BaseObject
*
* @param int $cid
* @return array
* @throws \Exception
*/
public static function getIdsByContactId($cid)
{
$condition = ['contact-id' => $cid];
$stmt = DBA::select('group_member', ['gid'], $condition);
$return = [];
$stmt = DBA::select('group_member', ['gid'], ['contact-id' => $cid]);
while ($group = DBA::fetch($stmt)) {
$return[] = $group['gid'];
}
DBA::close($stmt);
return $return;
}
@ -94,9 +127,10 @@ class Group extends BaseObject
* Count unread items of each groups of the local user
*
* @return array
* 'id' => group id
* 'name' => group name
* 'count' => counted unseen group items
* 'id' => group id
* 'name' => group name
* 'count' => counted unseen group items
* @throws \Exception
*/
public static function countUnseen()
{
@ -123,9 +157,10 @@ class Group extends BaseObject
*
* Returns false if no group has been found.
*
* @param int $uid
* @param int $uid
* @param string $name
* @return int|boolean
* @throws \Exception
*/
public static function getIdByName($uid, $name)
{
@ -146,9 +181,11 @@ class Group extends BaseObject
*
* @param int $gid
* @return boolean
* @throws \Exception
*/
public static function remove($gid) {
if (! $gid) {
public static function remove($gid)
{
if (!$gid) {
return false;
}
@ -190,17 +227,19 @@ class Group extends BaseObject
}
/**
* @brief Mark a group as deleted based on its name
* @brief Mark a group as deleted based on its name
*
* @deprecated Use Group::remove instead
*
* @param int $uid
* @param int $uid
* @param string $name
* @return bool
* @throws \Exception
* @deprecated Use Group::remove instead
*
*/
public static function removeByName($uid, $name) {
public static function removeByName($uid, $name)
{
$return = false;
if (x($uid) && x($name)) {
if (!empty($uid) && !empty($name)) {
$gid = self::getIdByName($uid, $name);
$return = self::remove($gid);
@ -215,6 +254,7 @@ class Group extends BaseObject
* @param int $gid
* @param int $cid
* @return boolean
* @throws \Exception
*/
public static function addMember($gid, $cid)
{
@ -239,6 +279,7 @@ class Group extends BaseObject
* @param int $gid
* @param int $cid
* @return boolean
* @throws \Exception
*/
public static function removeMember($gid, $cid)
{
@ -252,14 +293,15 @@ class Group extends BaseObject
}
/**
* @brief Removes a contact from a group based on its name
* @brief Removes a contact from a group based on its name
*
* @param int $uid
* @param string $name
* @param int $cid
* @return boolean
* @throws \Exception
* @deprecated Use Group::removeMember instead
*
* @param int $uid
* @param string $name
* @param int $cid
* @return boolean
*/
public static function removeMemberByName($uid, $name, $cid)
{
@ -273,22 +315,55 @@ class Group extends BaseObject
/**
* @brief Returns the combined list of contact ids from a group id list
*
* @param array $group_ids
* @param int $uid
* @param array $group_ids
* @param boolean $check_dead
* @return array
* @throws \Exception
*/
public static function expand($group_ids, $check_dead = false)
public static function expand($uid, array $group_ids, $check_dead = false)
{
if (!is_array($group_ids) || !count($group_ids)) {
return [];
}
$stmt = DBA::select('group_member', ['contact-id'], ['gid' => $group_ids]);
$return = [];
while($group_member = DBA::fetch($stmt)) {
$key = array_search(self::FOLLOWERS, $group_ids);
if ($key !== false) {
$followers = Contact::selectToArray(['id'], [
'uid' => $uid,
'rel' => [Contact::FOLLOWER, Contact::FRIEND],
'network' => Protocol::SUPPORT_PRIVATE,
]);
foreach ($followers as $follower) {
$return[] = $follower['id'];
}
unset($group_ids[$key]);
}
$key = array_search(self::MUTUALS, $group_ids);
if ($key !== false) {
$mutuals = Contact::selectToArray(['id'], [
'uid' => $uid,
'rel' => [Contact::FRIEND],
'network' => Protocol::SUPPORT_PRIVATE,
]);
foreach ($mutuals as $mutual) {
$return[] = $mutual['id'];
}
unset($group_ids[$key]);
}
$stmt = DBA::select('group_member', ['contact-id'], ['gid' => $group_ids]);
while ($group_member = DBA::fetch($stmt)) {
$return[] = $group_member['contact-id'];
}
DBA::close($stmt);
if ($check_dead) {
Contact::pruneUnavailable($return);
@ -300,17 +375,14 @@ class Group extends BaseObject
/**
* @brief Returns a templated group selection list
*
* @param int $uid
* @param int $gid An optional pre-selected group
* @param int $uid
* @param int $gid An optional pre-selected group
* @param string $label An optional label of the list
* @return string
* @throws \Exception
*/
public static function displayGroupSelection($uid, $gid = 0, $label = '')
{
$o = '';
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => $uid], ['order' => ['name']]);
$display_groups = [
[
'name' => '',
@ -318,6 +390,8 @@ class Group extends BaseObject
'selected' => ''
]
];
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => $uid], ['order' => ['name']]);
while ($group = DBA::fetch($stmt)) {
$display_groups[] = [
'name' => $group['name'],
@ -325,13 +399,15 @@ class Group extends BaseObject
'selected' => $gid == $group['id'] ? 'true' : ''
];
}
logger('groups: ' . print_r($display_groups, true));
DBA::close($stmt);
Logger::info('Got groups', $display_groups);
if ($label == '') {
$label = L10n::t('Default privacy group for new contacts');
}
$o = replace_macros(get_markup_template('group_selection.tpl'), [
$o = Renderer::replaceMacros(Renderer::getMarkupTemplate('group_selection.tpl'), [
'$label' => $label,
'$groups' => $display_groups
]);
@ -344,17 +420,16 @@ class Group extends BaseObject
* @param string $every
* @param string $each
* @param string $editmode
* 'standard' => include link 'Edit groups'
* 'extended' => include link 'Create new group'
* 'full' => include link 'Create new group' and provide for each group a link to edit this group
* @param int $group_id
* @param int $cid
* 'standard' => include link 'Edit groups'
* 'extended' => include link 'Create new group'
* 'full' => include link 'Create new group' and provide for each group a link to edit this group
* @param string $group_id
* @param int $cid
* @return string
* @throws \Exception
*/
public static function sidebarWidget($every = 'contact', $each = 'group', $editmode = 'standard', $group_id = '', $cid = 0)
{
$o = '';
if (!local_user()) {
return '';
}
@ -368,13 +443,12 @@ class Group extends BaseObject
]
];
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => local_user()], ['order' => ['name']]);
$member_of = [];
if ($cid) {
$member_of = self::getIdsByContactId($cid);
}
$stmt = DBA::select('group', [], ['deleted' => 0, 'uid' => local_user()], ['order' => ['name']]);
while ($group = DBA::fetch($stmt)) {
$selected = (($group_id == $group['id']) ? ' group-selected' : '');
@ -397,9 +471,15 @@ class Group extends BaseObject
'ismember' => in_array($group['id'], $member_of),
];
}
DBA::close($stmt);
$tpl = get_markup_template('group_side.tpl');
$o = replace_macros($tpl, [
// Don't show the groups on the network page when there is only one
if ((count($display_groups) <= 2) && ($each == 'network')) {
return '';
}
$tpl = Renderer::getMarkupTemplate('group_side.tpl');
$o = Renderer::replaceMacros($tpl, [
'$add' => L10n::t('add'),
'$title' => L10n::t('Groups'),
'$groups' => $display_groups,
@ -414,7 +494,6 @@ class Group extends BaseObject
'$form_security_token' => BaseModule::getFormSecurityToken('group_edit'),
]);
return $o;
}
}

File diff suppressed because it is too large Load diff

View file

@ -11,10 +11,6 @@ use Friendica\Content\Text;
use Friendica\Core\PConfig;
use Friendica\Core\Protocol;
require_once 'boot.php';
require_once 'include/items.php';
require_once 'include/text.php';
class ItemContent extends BaseObject
{
/**
@ -26,9 +22,10 @@ class ItemContent extends BaseObject
* @param int $htmlmode This controls the behavior of the BBCode conversion
* @param string $target_network Name of the network where the post should go to.
*
* @see \Friendica\Content\Text\BBCode::getAttachedData
*
* @return array Same array structure than \Friendica\Content\Text\BBCode::getAttachedData
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @see \Friendica\Content\Text\BBCode::getAttachedData
*
*/
public static function getPlaintextPost($item, $limit = 0, $includedlinks = false, $htmlmode = 2, $target_network = '')
{

View file

@ -0,0 +1,177 @@
<?php
/**
* @file src/Model/ItemDeliveryData.php
*/
namespace Friendica\Model;
use Friendica\Database\DBA;
use \BadMethodCallException;
class ItemDeliveryData
{
const LEGACY_FIELD_LIST = [
// Legacy fields moved from item table
'postopts',
'inform',
];
const FIELD_LIST = [
// New delivery fields with virtual field name in item fields
'queue_count' => 'delivery_queue_count',
'queue_done' => 'delivery_queue_done',
'queue_failed' => 'delivery_queue_failed',
];
const ACTIVITYPUB = 1;
const DFRN = 2;
const LEGACY_DFRN = 3;
const DIASPORA = 4;
const OSTATUS = 5;
/**
* Extract delivery data from the provided item fields
*
* @param array $fields
* @return array
*/
public static function extractFields(array &$fields)
{
$delivery_data = [];
foreach (array_merge(ItemDeliveryData::FIELD_LIST, ItemDeliveryData::LEGACY_FIELD_LIST) as $key => $field) {
if (is_int($key) && isset($fields[$field])) {
// Legacy field moved from item table
$delivery_data[$field] = $fields[$field];
$fields[$field] = null;
} elseif (isset($fields[$field])) {
// New delivery field with virtual field name in item fields
$delivery_data[$key] = $fields[$field];
unset($fields[$field]);
}
}
return $delivery_data;
}
/**
* Increments the queue_done for the given item ID.
*
* Avoids racing condition between multiple delivery threads.
*
* @param integer $item_id
* @param integer $protocol
* @return bool
* @throws \Exception
*/
public static function incrementQueueDone($item_id, $protocol = 0)
{
$sql = '';
switch ($protocol) {
case self::ACTIVITYPUB:
$sql = ", `activitypub` = `activitypub` + 1";
break;
case self::DFRN:
$sql = ", `dfrn` = `dfrn` + 1";
break;
case self::LEGACY_DFRN:
$sql = ", `legacy_dfrn` = `legacy_dfrn` + 1";
break;
case self::DIASPORA:
$sql = ", `diaspora` = `diaspora` + 1";
break;
case self::OSTATUS:
$sql = ", `ostatus` = `ostatus` + 1";
break;
}
return DBA::e('UPDATE `item-delivery-data` SET `queue_done` = `queue_done` + 1' . $sql . ' WHERE `iid` = ?', $item_id);
}
/**
* Increments the queue_failed for the given item ID.
*
* Avoids racing condition between multiple delivery threads.
*
* @param integer $item_id
* @return bool
* @throws \Exception
*/
public static function incrementQueueFailed($item_id)
{
return DBA::e('UPDATE `item-delivery-data` SET `queue_failed` = `queue_failed` + 1 WHERE `iid` = ?', $item_id);
}
/**
* Increments the queue_count for the given item ID.
*
* @param integer $item_id
* @param integer $increment
* @return bool
* @throws \Exception
*/
public static function incrementQueueCount(int $item_id, int $increment = 1)
{
return DBA::e('UPDATE `item-delivery-data` SET `queue_count` = `queue_count` + ? WHERE `iid` = ?', $increment, $item_id);
}
/**
* Insert a new item delivery data entry
*
* @param integer $item_id
* @param array $fields
* @return bool
* @throws \Exception
*/
public static function insert($item_id, array $fields)
{
if (empty($item_id)) {
throw new BadMethodCallException('Empty item_id');
}
$fields['iid'] = $item_id;
return DBA::insert('item-delivery-data', $fields);
}
/**
* Update/Insert item delivery data
*
* If you want to update queue_done, please use incrementQueueDone instead.
*
* @param integer $item_id
* @param array $fields
* @return bool
* @throws \Exception
*/
public static function update($item_id, array $fields)
{
if (empty($item_id)) {
throw new BadMethodCallException('Empty item_id');
}
if (empty($fields)) {
// Nothing to do, update successful
return true;
}
return DBA::update('item-delivery-data', $fields, ['iid' => $item_id], true);
}
/**
* Delete item delivery data
*
* @param integer $item_id
* @return bool
* @throws \Exception
*/
public static function delete($item_id)
{
if (empty($item_id)) {
throw new BadMethodCallException('Empty item_id');
}
return DBA::delete('item-delivery-data', ['iid' => $item_id]);
}
}

View file

@ -9,8 +9,6 @@ namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Database\DBA;
require_once 'boot.php';
class ItemURI extends BaseObject
{
/**
@ -19,14 +17,18 @@ class ItemURI extends BaseObject
* @param array $fields Item-uri fields
*
* @return integer item-uri id
* @throws \Exception
*/
public static function insert($fields)
{
if (!DBA::exists('item-uri', ['uri' => $fields['uri']])) {
// If the URI gets too long we only take the first parts and hope for best
$uri = substr($fields['uri'], 0, 255);
if (!DBA::exists('item-uri', ['uri' => $uri])) {
DBA::insert('item-uri', $fields, true);
}
$itemuri = DBA::selectFirst('item-uri', ['id'], ['uri' => $fields['uri']]);
$itemuri = DBA::selectFirst('item-uri', ['id'], ['uri' => $uri]);
if (!DBA::isResult($itemuri)) {
// This shouldn't happen
@ -42,9 +44,13 @@ class ItemURI extends BaseObject
* @param string $uri
*
* @return integer item-uri id
* @throws \Exception
*/
public static function getIdByURI($uri)
{
// If the URI gets too long we only take the first parts and hope for best
$uri = substr($uri, 0, 255);
$itemuri = DBA::selectFirst('item-uri', ['id'], ['uri' => $uri]);
if (!DBA::isResult($itemuri)) {

View file

@ -6,19 +6,91 @@
namespace Friendica\Model;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Model\Item;
use Friendica\Model\Photo;
use Friendica\Database\DBA;
use Friendica\Network\Probe;
use Friendica\Util\DateTimeFormat;
require_once 'include/dba.php';
use Friendica\Worker\Delivery;
/**
* Class to handle private messages
*/
class Mail
{
/**
* Insert received private message
*
* @param array $msg
* @return int|boolean Message ID or false on error
*/
public static function insert($msg)
{
$user = User::getById($msg['uid']);
if (!isset($msg['reply'])) {
$msg['reply'] = DBA::exists('mail', ['parent-uri' => $msg['parent-uri']]);
}
if (empty($msg['convid'])) {
$mail = DBA::selectFirst('mail', ['convid'], ["`convid` != 0 AND `parent-uri` = ?", $msg['parent-uri']]);
if (DBA::isResult($mail)) {
$msg['convid'] = $mail['convid'];
}
}
if (empty($msg['guid'])) {
$host = parse_url($msg['from-url'], PHP_URL_HOST);
$msg['guid'] = Item::guidFromUri($msg['uri'], $host);
}
$msg['created'] = (!empty($msg['created']) ? DateTimeFormat::utc($msg['created']) : DateTimeFormat::utcNow());
DBA::lock('mail');
if (DBA::exists('mail', ['uri' => $msg['uri'], 'uid' => $msg['uid']])) {
DBA::unlock();
Logger::info('duplicate message already delivered.');
return false;
}
DBA::insert('mail', $msg);
$msg['id'] = DBA::lastInsertId();
DBA::unlock();
if (!empty($msg['convid'])) {
DBA::update('conv', ['updated' => DateTimeFormat::utcNow()], ['id' => $msg['convid']]);
}
// send notifications.
$notif_params = [
'type' => NOTIFY_MAIL,
'notify_flags' => $user['notify-flags'],
'language' => $user['language'],
'to_name' => $user['username'],
'to_email' => $user['email'],
'uid' => $user['uid'],
'item' => $msg,
'parent' => 0,
'source_name' => $msg['from-name'],
'source_link' => $msg['from-url'],
'source_photo' => $msg['from-photo'],
'verb' => ACTIVITY_POST,
'otype' => 'mail'
];
notification($notif_params);
Logger::info('Mail is processed, notification was sent.', ['id' => $msg['id'], 'uri' => $msg['uri']]);
return $msg['id'];
}
/**
* Send private message
*
@ -26,10 +98,12 @@ class Mail
* @param string $body message body, default empty
* @param string $subject message subject, default empty
* @param string $replyto reply to
* @return int
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function send($recipient = 0, $body = '', $subject = '', $replyto = '')
{
$a = get_app();
$a = \get_app();
if (!$recipient) {
return -1;
@ -46,8 +120,10 @@ class Mail
return -2;
}
Photo::setPermissionFromBody($body, local_user(), $me['id'], '<' . $contact['id'] . '>', '', '', '');
$guid = System::createUUID();
$uri = 'urn:X-dfrn:' . System::baseUrl() . ':' . local_user() . ':' . $guid;
$uri = Item::newURI(local_user(), $guid);
$convid = 0;
$reply = false;
@ -87,7 +163,7 @@ class Mail
}
if (!$convid) {
logger('send message: conversation not found.');
Logger::log('send message: conversation not found.');
return -4;
}
@ -142,13 +218,13 @@ class Mail
}
$image_uri = substr($image, strrpos($image, '/') + 1);
$image_uri = substr($image_uri, 0, strpos($image_uri, '-'));
DBA::update('photo', ['allow-cid' => '<' . $recipient . '>'], ['resource-id' => $image_uri, 'album' => 'Wall Photos', 'uid' => local_user()]);
Photo::update(['allow-cid' => '<' . $recipient . '>'], ['resource-id' => $image_uri, 'album' => 'Wall Photos', 'uid' => local_user()]);
}
}
}
if ($post_id) {
Worker::add(PRIORITY_HIGH, "Notifier", "mail", $post_id);
Worker::add(PRIORITY_HIGH, "Notifier", Delivery::MAIL, $post_id);
return intval($post_id);
} else {
return -3;
@ -156,12 +232,15 @@ class Mail
}
/**
* @param string $recipient recipient, default empty
* @param array $recipient recipient, default empty
* @param string $body message body, default empty
* @param string $subject message subject, default empty
* @param string $replyto reply to, default empty
* @return int
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function sendWall($recipient = '', $body = '', $subject = '', $replyto = '')
public static function sendWall(array $recipient = [], $body = '', $subject = '', $replyto = '')
{
if (!$recipient) {
return -1;
@ -172,7 +251,7 @@ class Mail
}
$guid = System::createUUID();
$uri = 'urn:X-dfrn:' . System::baseUrl() . ':' . local_user() . ':' . $guid;
$uri = Item::newURI(local_user(), $guid);
$me = Probe::uri($replyto);
@ -200,7 +279,7 @@ class Mail
}
if (!$convid) {
logger('send message: conversation not found.');
Logger::log('send message: conversation not found.');
return -4;
}

64
src/Model/Nodeinfo.php Normal file
View file

@ -0,0 +1,64 @@
<?php
namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Core\Addon;
use Friendica\Database\DBA;
/**
* Model interaction for the nodeinfo
*/
class Nodeinfo extends BaseObject
{
/**
* Updates the info about the current node
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function update()
{
$app = self::getApp();
$config = $app->getConfig();
$logger = $app->getLogger();
// If the addon 'statistics_json' is enabled then disable it and activate nodeinfo.
if (Addon::isEnabled('statistics_json')) {
$config->set('system', 'nodeinfo', true);
$addon = 'statistics_json';
$addons = $config->get('system', 'addon');
if ($addons) {
$addons_arr = explode(',', str_replace(' ', '', $addons));
$idx = array_search($addon, $addons_arr);
if ($idx !== false) {
unset($addons_arr[$idx]);
Addon::uninstall($addon);
$config->set('system', 'addon', implode(', ', $addons_arr));
}
}
}
if (empty($config->get('system', 'nodeinfo'))) {
return;
}
$userStats = User::getStatistics();
$config->set('nodeinfo', 'total_users', $userStats['total_users']);
$config->set('nodeinfo', 'active_users_halfyear', $userStats['active_users_halfyear']);
$config->set('nodeinfo', 'active_users_monthly', $userStats['active_users_monthly']);
$logger->debug('user statistics', $userStats);
$local_posts = DBA::count('thread', ["`wall` AND NOT `deleted` AND `uid` != 0"]);
$config->set('nodeinfo', 'local_posts', $local_posts);
$logger->debug('thread statistics', ['local_posts' => $local_posts]);
$local_comments = DBA::count('item', ["`origin` AND `id` != `parent` AND NOT `deleted` AND `uid` != 0"]);
$config->set('nodeinfo', 'local_comments', $local_comments);
$logger->debug('item statistics', ['local_comments' => $local_comments]);
}
}

View file

@ -16,12 +16,13 @@ class OpenWebAuthToken
/**
* Create an entry in the 'openwebauth-token' table.
*
* @param string $type Verify type.
* @param int $uid The user ID.
* @param string $type Verify type.
* @param int $uid The user ID.
* @param string $token
* @param string $meta
*
* @return boolean
* @throws \Exception
*/
public static function create($type, $uid, $token, $meta)
{
@ -38,11 +39,12 @@ class OpenWebAuthToken
/**
* Get the "meta" field of an entry in the openwebauth-token table.
*
* @param string $type Verify type.
* @param int $uid The user ID.
* @param string $type Verify type.
* @param int $uid The user ID.
* @param string $token
*
* @return string|boolean The meta enry or false if not found.
* @throws \Exception
*/
public static function getMeta($type, $uid, $token)
{
@ -62,6 +64,7 @@ class OpenWebAuthToken
*
* @param string $type Verify type.
* @param string $interval SQL compatible time interval
* @throws \Exception
*/
public static function purge($type, $interval)
{

View file

@ -7,8 +7,6 @@ namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Database\DBA;
require_once 'include/dba.php';
/**
* @brief functions for interacting with the permission set of an object (item, photo, event, ...)
*/
@ -18,7 +16,8 @@ class PermissionSet extends BaseObject
* Fetch the id of a given permission set. Generate a new one when needed
*
* @param array $postarray The array from an item, picture or event post
* @return id
* @return int id
* @throws \Exception
*/
public static function fetchIDForPost(&$postarray)
{
@ -68,20 +67,20 @@ class PermissionSet extends BaseObject
*
* @param integer $uid User id whom the items belong
* @param integer $contact_id Contact id of the visitor
* @param array $groups Possibly previously fetched group ids for that contact
*
* @return array of permission set ids.
* @throws \Exception
*/
static public function get($uid, $contact_id, $groups = null)
static public function get($uid, $contact_id)
{
if (empty($groups) && DBA::exists('contact', ['id' => $contact_id, 'uid' => $uid, 'blocked' => false])) {
if (DBA::exists('contact', ['id' => $contact_id, 'uid' => $uid, 'blocked' => false])) {
$groups = Group::getIdsByContactId($contact_id);
}
if (empty($groups) || !is_array($groups)) {
return [];
}
$group_str = '<<>>'; // should be impossible to match
foreach ($groups as $g) {
@ -90,11 +89,9 @@ class PermissionSet extends BaseObject
$contact_str = '<' . $contact_id . '>';
$condition = ["`uid` = ? AND (`allow_cid` = '' OR`allow_cid` REGEXP ?)
AND (`deny_cid` = '' OR NOT `deny_cid` REGEXP ?)
AND (`allow_gid` = '' OR `allow_gid` REGEXP ?)
AND (`deny_gid` = '' OR NOT `deny_gid` REGEXP ?)",
$uid, $contact_str, $contact_str, $group_str, $group_str];
$condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR deny_gid REGEXP ?)
AND (allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))",
$uid, $contact_str, $group_str, $contact_str, $group_str];
$ret = DBA::select('permissionset', ['id'], $condition);
$set = [];

View file

@ -6,99 +6,405 @@
*/
namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Core\Cache;
use Friendica\Core\Config;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\StorageManager;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Database\DBStructure;
use Friendica\Model\Storage\IStorage;
use Friendica\Object\Image;
use Friendica\Protocol\DFRN;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network;
use Friendica\Util\Security;
use Friendica\Util\Strings;
require_once 'include/dba.php';
require_once "include/dba.php";
/**
* Class to handle photo dabatase table
*/
class Photo
class Photo extends BaseObject
{
/**
* @param Image $Image image
* @param integer $uid uid
* @param integer $cid cid
* @param integer $rid rid
* @param string $filename filename
* @param string $album album name
* @param integer $scale scale
* @param integer $profile optional, default = 0
* @param string $allow_cid optional, default = ''
* @param string $allow_gid optional, default = ''
* @param string $deny_cid optional, default = ''
* @param string $deny_gid optional, default = ''
* @param string $desc optional, default = ''
* @return object
* @brief Select rows from the photo table and returns them as array
*
* @param array $fields Array of selected fields, empty for all
* @param array $conditions Array of fields for conditions
* @param array $params Array of several parameters
*
* @return boolean|array
*
* @throws \Exception
* @see \Friendica\Database\DBA::selectToArray
*/
public static function store(Image $Image, $uid, $cid, $rid, $filename, $album, $scale, $profile = 0, $allow_cid = '', $allow_gid = '', $deny_cid = '', $deny_gid = '', $desc = '')
public static function selectToArray(array $fields = [], array $conditions = [], array $params = [])
{
$photo = DBA::selectFirst('photo', ['guid'], ["`resource-id` = ? AND `guid` != ?", $rid, '']);
if (empty($fields)) {
$fields = self::getFields();
}
return DBA::selectToArray('photo', $fields, $conditions, $params);
}
/**
* @brief Retrieve a single record from the photo table
*
* @param array $fields Array of selected fields, empty for all
* @param array $conditions Array of fields for conditions
* @param array $params Array of several parameters
*
* @return bool|array
*
* @throws \Exception
* @see \Friendica\Database\DBA::select
*/
public static function selectFirst(array $fields = [], array $conditions = [], array $params = [])
{
if (empty($fields)) {
$fields = self::getFields();
}
return DBA::selectFirst("photo", $fields, $conditions, $params);
}
/**
* @brief Get photos for user id
*
* @param integer $uid User id
* @param string $resourceid Rescource ID of the photo
* @param array $conditions Array of fields for conditions
* @param array $params Array of several parameters
*
* @return bool|array
*
* @throws \Exception
* @see \Friendica\Database\DBA::select
*/
public static function getPhotosForUser($uid, $resourceid, array $conditions = [], array $params = [])
{
$conditions["resource-id"] = $resourceid;
$conditions["uid"] = $uid;
return self::selectToArray([], $conditions, $params);
}
/**
* @brief Get a photo for user id
*
* @param integer $uid User id
* @param string $resourceid Rescource ID of the photo
* @param integer $scale Scale of the photo. Defaults to 0
* @param array $conditions Array of fields for conditions
* @param array $params Array of several parameters
*
* @return bool|array
*
* @throws \Exception
* @see \Friendica\Database\DBA::select
*/
public static function getPhotoForUser($uid, $resourceid, $scale = 0, array $conditions = [], array $params = [])
{
$conditions["resource-id"] = $resourceid;
$conditions["uid"] = $uid;
$conditions["scale"] = $scale;
return self::selectFirst([], $conditions, $params);
}
/**
* @brief Get a single photo given resource id and scale
*
* This method checks for permissions. Returns associative array
* on success, "no sign" image info, if user has no permission,
* false if photo does not exists
*
* @param string $resourceid Rescource ID of the photo
* @param integer $scale Scale of the photo. Defaults to 0
*
* @return boolean|array
* @throws \Exception
*/
public static function getPhoto($resourceid, $scale = 0)
{
$r = self::selectFirst(["uid"], ["resource-id" => $resourceid]);
if (!DBA::isResult($r)) {
return false;
}
$uid = $r["uid"];
$sql_acl = Security::getPermissionsSQLByUserId($uid);
$conditions = ["`resource-id` = ? AND `scale` <= ? " . $sql_acl, $resourceid, $scale];
$params = ["order" => ["scale" => true]];
$photo = self::selectFirst([], $conditions, $params);
return $photo;
}
/**
* @brief Check if photo with given conditions exists
*
* @param array $conditions Array of extra conditions
*
* @return boolean
* @throws \Exception
*/
public static function exists(array $conditions)
{
return DBA::exists("photo", $conditions);
}
/**
* @brief Get Image object for given row id. null if row id does not exist
*
* @param array $photo Photo data. Needs at least 'id', 'type', 'backend-class', 'backend-ref'
*
* @return \Friendica\Object\Image
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function getImageForPhoto(array $photo)
{
$data = "";
if ($photo["backend-class"] == "") {
// legacy data storage in "data" column
$i = self::selectFirst(["data"], ["id" => $photo["id"]]);
if ($i === false) {
return null;
}
$data = $i["data"];
} else {
$backendClass = $photo["backend-class"];
$backendRef = $photo["backend-ref"];
$data = $backendClass::get($backendRef);
}
if ($data === "") {
return null;
}
return new Image($data, $photo["type"]);
}
/**
* @brief Return a list of fields that are associated with the photo table
*
* @return array field list
* @throws \Exception
*/
private static function getFields()
{
$allfields = DBStructure::definition(self::getApp()->getBasePath(), false);
$fields = array_keys($allfields["photo"]["fields"]);
array_splice($fields, array_search("data", $fields), 1);
return $fields;
}
/**
* @brief Construct a photo array for a system resource image
*
* @param string $filename Image file name relative to code root
* @param string $mimetype Image mime type. Defaults to "image/jpeg"
*
* @return array
* @throws \Exception
*/
public static function createPhotoForSystemResource($filename, $mimetype = "image/jpeg")
{
$fields = self::getFields();
$values = array_fill(0, count($fields), "");
$photo = array_combine($fields, $values);
$photo["backend-class"] = Storage\SystemResource::class;
$photo["backend-ref"] = $filename;
$photo["type"] = $mimetype;
$photo["cacheable"] = false;
return $photo;
}
/**
* @brief store photo metadata in db and binary in default backend
*
* @param Image $Image Image object with data
* @param integer $uid User ID
* @param integer $cid Contact ID
* @param integer $rid Resource ID
* @param string $filename Filename
* @param string $album Album name
* @param integer $scale Scale
* @param integer $profile Is a profile image? optional, default = 0
* @param string $allow_cid Permissions, allowed contacts. optional, default = ""
* @param string $allow_gid Permissions, allowed groups. optional, default = ""
* @param string $deny_cid Permissions, denied contacts.optional, default = ""
* @param string $deny_gid Permissions, denied greoup.optional, default = ""
* @param string $desc Photo caption. optional, default = ""
*
* @return boolean True on success
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function store(Image $Image, $uid, $cid, $rid, $filename, $album, $scale, $profile = 0, $allow_cid = "", $allow_gid = "", $deny_cid = "", $deny_gid = "", $desc = "")
{
$photo = self::selectFirst(["guid"], ["`resource-id` = ? AND `guid` != ?", $rid, ""]);
if (DBA::isResult($photo)) {
$guid = $photo['guid'];
$guid = $photo["guid"];
} else {
$guid = System::createGUID();
}
$existing_photo = DBA::selectFirst('photo', ['id'], ['resource-id' => $rid, 'uid' => $uid, 'contact-id' => $cid, 'scale' => $scale]);
$existing_photo = self::selectFirst(["id", "created", "backend-class", "backend-ref"], ["resource-id" => $rid, "uid" => $uid, "contact-id" => $cid, "scale" => $scale]);
$created = DateTimeFormat::utcNow();
if (DBA::isResult($existing_photo)) {
$created = $existing_photo["created"];
}
// Get defined storage backend.
// if no storage backend, we use old "data" column in photo table.
// if is an existing photo, reuse same backend
$data = "";
$backend_ref = "";
/** @var IStorage $backend_class */
if (DBA::isResult($existing_photo)) {
$backend_ref = (string)$existing_photo["backend-ref"];
$backend_class = (string)$existing_photo["backend-class"];
} else {
$backend_class = StorageManager::getBackend();
}
if ($backend_class === "") {
$data = $Image->asString();
} else {
$backend_ref = $backend_class::put($Image->asString(), $backend_ref);
}
$fields = [
'uid' => $uid,
'contact-id' => $cid,
'guid' => $guid,
'resource-id' => $rid,
'created' => DateTimeFormat::utcNow(),
'edited' => DateTimeFormat::utcNow(),
'filename' => basename($filename),
'type' => $Image->getType(),
'album' => $album,
'height' => $Image->getHeight(),
'width' => $Image->getWidth(),
'datasize' => strlen($Image->asString()),
'data' => $Image->asString(),
'scale' => $scale,
'profile' => $profile,
'allow_cid' => $allow_cid,
'allow_gid' => $allow_gid,
'deny_cid' => $deny_cid,
'deny_gid' => $deny_gid,
'desc' => $desc
"uid" => $uid,
"contact-id" => $cid,
"guid" => $guid,
"resource-id" => $rid,
"created" => $created,
"edited" => DateTimeFormat::utcNow(),
"filename" => basename($filename),
"type" => $Image->getType(),
"album" => $album,
"height" => $Image->getHeight(),
"width" => $Image->getWidth(),
"datasize" => strlen($Image->asString()),
"data" => $data,
"scale" => $scale,
"profile" => $profile,
"allow_cid" => $allow_cid,
"allow_gid" => $allow_gid,
"deny_cid" => $deny_cid,
"deny_gid" => $deny_gid,
"desc" => $desc,
"backend-class" => $backend_class,
"backend-ref" => $backend_ref
];
if (DBA::isResult($existing_photo)) {
$r = DBA::update('photo', $fields, ['id' => $existing_photo['id']]);
$r = DBA::update("photo", $fields, ["id" => $existing_photo["id"]]);
} else {
$r = DBA::insert('photo', $fields);
$r = DBA::insert("photo", $fields);
}
return $r;
}
/**
* @brief Delete info from table and data from storage
*
* @param array $conditions Field condition(s)
* @param array $options Options array, Optional
*
* @return boolean
*
* @throws \Exception
* @see \Friendica\Database\DBA::delete
*/
public static function delete(array $conditions, array $options = [])
{
// get photo to delete data info
$photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions);
foreach($photos as $photo) {
/** @var IStorage $backend_class */
$backend_class = (string)$photo["backend-class"];
if ($backend_class !== "") {
$backend_class::delete($photo["backend-ref"]);
}
}
return DBA::delete("photo", $conditions, $options);
}
/**
* @brief Update a photo
*
* @param array $fields Contains the fields that are updated
* @param array $conditions Condition array with the key values
* @param Image $img Image to update. Optional, default null.
* @param array|boolean $old_fields Array with the old field values that are about to be replaced (true = update on duplicate)
*
* @return boolean Was the update successfull?
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @see \Friendica\Database\DBA::update
*/
public static function update($fields, $conditions, Image $img = null, array $old_fields = [])
{
if (!is_null($img)) {
// get photo to update
$photos = self::selectToArray(['backend-class', 'backend-ref'], $conditions);
foreach($photos as $photo) {
/** @var IStorage $backend_class */
$backend_class = (string)$photo["backend-class"];
if ($backend_class !== "") {
$fields["backend-ref"] = $backend_class::put($img->asString(), $photo["backend-ref"]);
} else {
$fields["data"] = $img->asString();
}
}
$fields['updated'] = DateTimeFormat::utcNow();
}
$fields['edited'] = DateTimeFormat::utcNow();
return DBA::update("photo", $fields, $conditions, $old_fields);
}
/**
* @param string $image_url Remote URL
* @param integer $uid user id
* @param integer $cid contact id
* @param boolean $quit_on_error optional, default false
* @return array
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function importProfilePhoto($image_url, $uid, $cid, $quit_on_error = false)
{
$thumb = '';
$micro = '';
$thumb = "";
$micro = "";
$photo = DBA::selectFirst(
'photo', ['resource-id'], ['uid' => $uid, 'contact-id' => $cid, 'scale' => 4, 'album' => 'Contact Photos']
"photo", ["resource-id"], ["uid" => $uid, "contact-id" => $cid, "scale" => 4, "album" => "Contact Photos"]
);
if (x($photo['resource-id'])) {
$hash = $photo['resource-id'];
if (!empty($photo['resource-id'])) {
$hash = $photo["resource-id"];
} else {
$hash = self::newResource();
}
@ -106,18 +412,27 @@ class Photo
$photo_failure = false;
$filename = basename($image_url);
$img_str = Network::fetchUrl($image_url, true);
if (!empty($image_url)) {
$ret = Network::curl($image_url, true);
$img_str = $ret->getBody();
$type = $ret->getContentType();
} else {
$img_str = '';
}
if ($quit_on_error && ($img_str == "")) {
return false;
}
$type = Image::guessType($image_url, true);
if (empty($type)) {
$type = Image::guessType($image_url, true);
}
$Image = new Image($img_str, $type);
if ($Image->isValid()) {
$Image->scaleToSquare(300);
$r = self::store($Image, $uid, $cid, $hash, $filename, 'Contact Photos', 4);
$r = self::store($Image, $uid, $cid, $hash, $filename, "Contact Photos", 4);
if ($r === false) {
$photo_failure = true;
@ -125,7 +440,7 @@ class Photo
$Image->scaleDown(80);
$r = self::store($Image, $uid, $cid, $hash, $filename, 'Contact Photos', 5);
$r = self::store($Image, $uid, $cid, $hash, $filename, "Contact Photos", 5);
if ($r === false) {
$photo_failure = true;
@ -133,32 +448,32 @@ class Photo
$Image->scaleDown(48);
$r = self::store($Image, $uid, $cid, $hash, $filename, 'Contact Photos', 6);
$r = self::store($Image, $uid, $cid, $hash, $filename, "Contact Photos", 6);
if ($r === false) {
$photo_failure = true;
}
$suffix = '?ts=' . time();
$suffix = "?ts=" . time();
$image_url = System::baseUrl() . '/photo/' . $hash . '-4.' . $Image->getExt() . $suffix;
$thumb = System::baseUrl() . '/photo/' . $hash . '-5.' . $Image->getExt() . $suffix;
$micro = System::baseUrl() . '/photo/' . $hash . '-6.' . $Image->getExt() . $suffix;
$image_url = System::baseUrl() . "/photo/" . $hash . "-4." . $Image->getExt() . $suffix;
$thumb = System::baseUrl() . "/photo/" . $hash . "-5." . $Image->getExt() . $suffix;
$micro = System::baseUrl() . "/photo/" . $hash . "-6." . $Image->getExt() . $suffix;
// Remove the cached photo
$a = get_app();
$a = \get_app();
$basepath = $a->getBasePath();
if (is_dir($basepath . "/photo")) {
$filename = $basepath . '/photo/' . $hash . '-4.' . $Image->getExt();
$filename = $basepath . "/photo/" . $hash . "-4." . $Image->getExt();
if (file_exists($filename)) {
unlink($filename);
}
$filename = $basepath . '/photo/' . $hash . '-5.' . $Image->getExt();
$filename = $basepath . "/photo/" . $hash . "-5." . $Image->getExt();
if (file_exists($filename)) {
unlink($filename);
}
$filename = $basepath . '/photo/' . $hash . '-6.' . $Image->getExt();
$filename = $basepath . "/photo/" . $hash . "-6." . $Image->getExt();
if (file_exists($filename)) {
unlink($filename);
}
@ -172,16 +487,16 @@ class Photo
}
if ($photo_failure) {
$image_url = System::baseUrl() . '/images/person-300.jpg';
$thumb = System::baseUrl() . '/images/person-80.jpg';
$micro = System::baseUrl() . '/images/person-48.jpg';
$image_url = System::baseUrl() . "/images/person-300.jpg";
$thumb = System::baseUrl() . "/images/person-80.jpg";
$micro = System::baseUrl() . "/images/person-48.jpg";
}
return [$image_url, $thumb, $micro];
}
/**
* @param string $exifCoord coordinate
* @param array $exifCoord coordinate
* @param string $hemi hemi
* @return float
*/
@ -191,7 +506,7 @@ class Photo
$minutes = count($exifCoord) > 1 ? self::gps2Num($exifCoord[1]) : 0;
$seconds = count($exifCoord) > 2 ? self::gps2Num($exifCoord[2]) : 0;
$flip = ($hemi == 'W' || $hemi == 'S') ? -1 : 1;
$flip = ($hemi == "W" || $hemi == "S") ? -1 : 1;
return floatval($flip * ($degrees + ($minutes / 60) + ($seconds / 3600)));
}
@ -202,7 +517,7 @@ class Photo
*/
private static function gps2Num($coordPart)
{
$parts = explode('/', $coordPart);
$parts = explode("/", $coordPart);
if (count($parts) <= 0) {
return 0;
@ -224,6 +539,7 @@ class Photo
* @param bool $update Update the cache
*
* @return array Returns array of the photo albums
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getAlbums($uid, $update = false)
{
@ -232,7 +548,7 @@ class Photo
$key = "photo_albums:".$uid.":".local_user().":".remote_user();
$albums = Cache::get($key);
if (is_null($albums) || $update) {
if (!Config::get('system', 'no_count', false)) {
if (!Config::get("system", "no_count", false)) {
/// @todo This query needs to be renewed. It is really slow
// At this time we just store the data in the cache
$albums = q("SELECT COUNT(DISTINCT `resource-id`) AS `total`, `album`, ANY_VALUE(`created`) AS `created`
@ -240,8 +556,8 @@ class Photo
WHERE `uid` = %d AND `album` != '%s' AND `album` != '%s' $sql_extra
GROUP BY `album` ORDER BY `created` DESC",
intval($uid),
DBA::escape('Contact Photos'),
DBA::escape(L10n::t('Contact Photos'))
DBA::escape("Contact Photos"),
DBA::escape(L10n::t("Contact Photos"))
);
} else {
// This query doesn't do the count and is much faster
@ -249,8 +565,8 @@ class Photo
FROM `photo` USE INDEX (`uid_album_scale_created`)
WHERE `uid` = %d AND `album` != '%s' AND `album` != '%s' $sql_extra",
intval($uid),
DBA::escape('Contact Photos'),
DBA::escape(L10n::t('Contact Photos'))
DBA::escape("Contact Photos"),
DBA::escape(L10n::t("Contact Photos"))
);
}
Cache::set($key, $albums, Cache::DAY);
@ -261,6 +577,7 @@ class Photo
/**
* @param int $uid User id of the photos
* @return void
* @throws \Exception
*/
public static function clearAlbumCache($uid)
{
@ -272,9 +589,130 @@ class Photo
* Generate a unique photo ID.
*
* @return string
* @throws \Exception
*/
public static function newResource()
{
return system::createGUID(32, false);
return System::createGUID(32, false);
}
/**
* Changes photo permissions that had been embedded in a post
*
* @todo This function currently does have some flaws:
* - Sharing a post with a forum will create a photo that only the forum can see.
* - Sharing a photo again that been shared non public before doesn't alter the permissions.
*
* @return string
* @throws \Exception
*/
public static function setPermissionFromBody($body, $uid, $original_contact_id, $str_contact_allow, $str_group_allow, $str_contact_deny, $str_group_deny)
{
// Simplify image codes
$img_body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
$img_body = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $img_body);
// Search for images
if (!preg_match_all("/\[img\](.*?)\[\/img\]/", $img_body, $match)) {
return false;
}
$images = $match[1];
if (empty($images)) {
return false;
}
foreach ($images as $image) {
if (!stristr($image, System::baseUrl() . '/photo/')) {
continue;
}
$image_uri = substr($image,strrpos($image,'/') + 1);
$image_uri = substr($image_uri,0, strpos($image_uri,'-'));
if (!strlen($image_uri)) {
continue;
}
// Ensure to only modify photos that you own
$srch = '<' . intval($original_contact_id) . '>';
$condition = [
'allow_cid' => $srch, 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => '',
'resource-id' => $image_uri, 'uid' => $uid
];
if (!Photo::exists($condition)) {
continue;
}
/// @todo Check if $str_contact_allow does contain a public forum. Then set the permissions to public.
$fields = ['allow_cid' => $str_contact_allow, 'allow_gid' => $str_group_allow,
'deny_cid' => $str_contact_deny, 'deny_gid' => $str_group_deny];
$condition = ['resource-id' => $image_uri, 'uid' => $uid];
Logger::info('Set permissions', ['condition' => $condition, 'permissions' => $fields]);
Photo::update($fields, $condition);
}
return true;
}
/**
* Strips known picture extensions from picture links
*
* @param string $name Picture link
* @return string stripped picture link
* @throws \Exception
*/
public static function stripExtension($name)
{
$name = str_replace([".jpg", ".png", ".gif"], ["", "", ""], $name);
foreach (Image::supportedTypes() as $m => $e) {
$name = str_replace("." . $e, "", $name);
}
return $name;
}
/**
* Returns the GUID from picture links
*
* @param string $name Picture link
* @return string GUID
* @throws \Exception
*/
public static function getGUID($name)
{
$a = \get_app();
$base = $a->getBaseURL();
$guid = str_replace([Strings::normaliseLink($base), '/photo/'], '', Strings::normaliseLink($name));
$guid = self::stripExtension($guid);
if (substr($guid, -2, 1) != "-") {
return '';
}
$scale = intval(substr($guid, -1, 1));
if (!is_numeric($scale)) {
return '';
}
$guid = substr($guid, 0, -2);
return $guid;
}
/**
* Tests if the picture link points to a locally stored picture
*
* @param string $name Picture link
* @return boolean
* @throws \Exception
*/
public static function isLocal($name)
{
$guid = self::getGUID($name);
if (empty($guid)) {
return false;
}
return DBA::exists('photo', ['resource-id' => $guid]);
}
}

View file

@ -8,8 +8,6 @@ use Friendica\BaseObject;
use Friendica\Database\DBA;
use Friendica\Util\DateTimeFormat;
require_once 'include/dba.php';
/**
* @brief functions for interacting with a process
*/
@ -21,6 +19,7 @@ class Process extends BaseObject
* @param string $command
* @param string $pid
* @return bool
* @throws \Exception
*/
public static function insert($command, $pid = null)
{
@ -46,6 +45,7 @@ class Process extends BaseObject
*
* @param string $pid
* @return bool
* @throws \Exception
*/
public static function deleteByPid($pid = null)
{

File diff suppressed because it is too large Load diff

View file

@ -4,19 +4,19 @@
*/
namespace Friendica\Model;
use Friendica\Core\Logger;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\Util\DateTimeFormat;
require_once 'include/dba.php';
class PushSubscriber
{
/**
* @brief Send subscription notifications for the given user
*
* @param integer $uid User ID
* @param string $priority Priority for push workers
* @param integer $uid User ID
* @param int $default_priority
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function publishFeed($uid, $default_priority = PRIORITY_HIGH)
{
@ -29,7 +29,8 @@ class PushSubscriber
/**
* @brief start workers to transmit the feed data
*
* @param string $priority Priority for push workers
* @param int $default_priority
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function requeue($default_priority = PRIORITY_HIGH)
{
@ -45,7 +46,7 @@ class PushSubscriber
$priority = $default_priority;
}
logger('Publish feed to ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' with priority ' . $priority, LOGGER_DEBUG);
Logger::log('Publish feed to ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' with priority ' . $priority, Logger::DEBUG);
Worker::add($priority, 'PubSubPublish', (int)$subscriber['id']);
}
@ -61,6 +62,7 @@ class PushSubscriber
* @param string $hub_callback Callback address
* @param string $hub_topic Feed topic
* @param string $hub_secret Subscription secret
* @throws \Exception
*/
public static function renew($uid, $nick, $subscribe, $hub_callback, $hub_topic, $hub_secret)
{
@ -88,9 +90,9 @@ class PushSubscriber
'secret' => $hub_secret];
DBA::insert('push_subscriber', $fields);
logger("Successfully subscribed [$hub_callback] for $nick");
Logger::log("Successfully subscribed [$hub_callback] for $nick");
} else {
logger("Successfully unsubscribed [$hub_callback] for $nick");
Logger::log("Successfully unsubscribed [$hub_callback] for $nick");
// we do nothing here, since the row was already deleted
}
}
@ -99,6 +101,7 @@ class PushSubscriber
* @brief Delay the push subscriber
*
* @param integer $id Subscriber ID
* @throws \Exception
*/
public static function delay($id)
{
@ -115,10 +118,10 @@ class PushSubscriber
if ($days > 60) {
DBA::update('push_subscriber', ['push' => -1, 'next_try' => DBA::NULL_DATETIME], ['id' => $id]);
logger('Delivery error: Subscription ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' is marked as ended.', LOGGER_DEBUG);
Logger::log('Delivery error: Subscription ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' is marked as ended.', Logger::DEBUG);
} else {
DBA::update('push_subscriber', ['push' => 0, 'next_try' => DBA::NULL_DATETIME], ['id' => $id]);
logger('Delivery error: Giving up ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' for now.', LOGGER_DEBUG);
Logger::log('Delivery error: Giving up ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' for now.', Logger::DEBUG);
}
} else {
// Calculate the delay until the next trial
@ -128,7 +131,7 @@ class PushSubscriber
$retrial = $retrial + 1;
DBA::update('push_subscriber', ['push' => $retrial, 'next_try' => $next], ['id' => $id]);
logger('Delivery error: Next try (' . $retrial . ') ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' at ' . $next, LOGGER_DEBUG);
Logger::log('Delivery error: Next try (' . $retrial . ') ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' at ' . $next, Logger::DEBUG);
}
}
@ -136,7 +139,8 @@ class PushSubscriber
* @brief Reset the push subscriber
*
* @param integer $id Subscriber ID
* @param date $last_update Date of last transmitted item
* @param string $last_update Date of last transmitted item
* @throws \Exception
*/
public static function reset($id, $last_update)
{
@ -148,6 +152,6 @@ class PushSubscriber
// set last_update to the 'created' date of the last item, and reset push=0
$fields = ['push' => 0, 'next_try' => DBA::NULL_DATETIME, 'last_update' => $last_update];
DBA::update('push_subscriber', $fields, ['id' => $id]);
logger('Subscriber ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' is marked as vital', LOGGER_DEBUG);
Logger::log('Subscriber ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' is marked as vital', Logger::DEBUG);
}
}

View file

@ -1,122 +0,0 @@
<?php
/**
* @file src/Model/Queue.php
*/
namespace Friendica\Model;
use Friendica\Core\Config;
use Friendica\Database\DBA;
use Friendica\Util\DateTimeFormat;
require_once 'include/dba.php';
class Queue
{
/**
* @param string $id id
*/
public static function updateTime($id)
{
logger('queue: requeue item ' . $id);
$queue = DBA::selectFirst('queue', ['retrial'], ['id' => $id]);
if (!DBA::isResult($queue)) {
return;
}
$retrial = $queue['retrial'];
if ($retrial > 14) {
self::removeItem($id);
}
// Calculate the delay until the next trial
$delay = (($retrial + 3) ** 4) + (rand(1, 30) * ($retrial + 1));
$next = DateTimeFormat::utc('now + ' . $delay . ' seconds');
DBA::update('queue', ['last' => DateTimeFormat::utcNow(), 'retrial' => $retrial + 1, 'next' => $next], ['id' => $id]);
}
/**
* @param string $id id
*/
public static function removeItem($id)
{
logger('queue: remove queue item ' . $id);
DBA::delete('queue', ['id' => $id]);
}
/**
* @brief Checks if the communication with a given contact had problems recently
*
* @param int $cid Contact id
*
* @return bool The communication with this contact has currently problems
*/
public static function wasDelayed($cid)
{
// Are there queue entries that were recently added?
$r = q("SELECT `id` FROM `queue` WHERE `cid` = %d
AND `last` > UTC_TIMESTAMP() - INTERVAL 15 MINUTE LIMIT 1",
intval($cid)
);
$was_delayed = DBA::isResult($r);
// We set "term-date" to a current date if the communication has problems.
// If the communication works again we reset this value.
if ($was_delayed) {
$r = q("SELECT `term-date` FROM `contact` WHERE `id` = %d AND `term-date` <= '1000-01-01' LIMIT 1",
intval($cid)
);
$was_delayed = !DBA::isResult($r);
}
return $was_delayed;
}
/**
* @param string $cid cid
* @param string $network network
* @param string $msg message
* @param boolean $batch batch, default false
*/
public static function add($cid, $network, $msg, $batch = false, $guid = '')
{
$max_queue = Config::get('system', 'max_contact_queue');
if ($max_queue < 1) {
$max_queue = 500;
}
$batch_queue = Config::get('system', 'max_batch_queue');
if ($batch_queue < 1) {
$batch_queue = 1000;
}
$r = q("SELECT COUNT(*) AS `total` FROM `queue` INNER JOIN `contact` ON `queue`.`cid` = `contact`.`id`
WHERE `queue`.`cid` = %d AND `contact`.`self` = 0 ",
intval($cid)
);
if (DBA::isResult($r)) {
if ($batch && ($r[0]['total'] > $batch_queue)) {
logger('too many queued items for batch server ' . $cid . ' - discarding message');
return;
} elseif ((! $batch) && ($r[0]['total'] > $max_queue)) {
logger('too many queued items for contact ' . $cid . ' - discarding message');
return;
}
}
DBA::insert('queue', [
'cid' => $cid,
'network' => $network,
'guid' => $guid,
'created' => DateTimeFormat::utcNow(),
'last' => DateTimeFormat::utcNow(),
'content' => $msg,
'batch' =>($batch) ? 1 : 0
]);
logger('Added item ' . $guid . ' for ' . $cid);
}
}

5
src/Model/README.md Normal file
View file

@ -0,0 +1,5 @@
## Friendica\Model
Models are the glue between the business logic of the app and the datastore(s).
In the namespace Model should only be static classes that interact with the DB with the same name as a database table.

View file

@ -3,10 +3,12 @@
/**
* @file src/Model/Register.php
*/
namespace Friendica\Model;
use Friendica\Database\DBA;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings;
/**
* Class interacting with the register database table
@ -19,11 +21,12 @@ class Register
* Return the list of pending registrations
*
* @return array
* @throws \Exception
*/
public static function getPending()
{
$stmt = DBA::p(
"SELECT `register`.*, `contact`.`name`, `user`.`email`
"SELECT `register`.*, `contact`.`name`, `contact`.`url`, `contact`.`micro`, `user`.`email`
FROM `register`
INNER JOIN `contact` ON `register`.`uid` = `contact`.`uid`
INNER JOIN `user` ON `register`.`uid` = `user`.`uid`"
@ -36,6 +39,7 @@ class Register
* Returns the pending registration count
*
* @return int
* @throws \Exception
*/
public static function getPendingCount()
{
@ -53,6 +57,7 @@ class Register
*
* @param string $hash
* @return array
* @throws \Exception
*/
public static function getByHash($hash)
{
@ -62,8 +67,9 @@ class Register
/**
* Returns true if a register record exists with the provided hash
*
* @param string $hash
* @param string $hash
* @return boolean
* @throws \Exception
*/
public static function existsByHash($hash)
{
@ -74,10 +80,11 @@ class Register
* Creates a register record for an invitation and returns the auto-generated code for it
*
* @return string
* @throws \Exception
*/
public static function createForInvitation()
{
$code = autoname(8) . srand(1000, 9999);
$code = Strings::getRandomName(8) . random_int(1000, 9999);
$fields = [
'hash' => $code,
@ -97,10 +104,11 @@ class Register
* @param string $language The registration language
* @param string $note An additional message from the user
* @return boolean
* @throws \Exception
*/
public static function createForApproval($uid, $language, $note = '')
{
$hash = random_string();
$hash = Strings::getRandomHex();
if (!User::exists($uid)) {
return false;
@ -121,8 +129,9 @@ class Register
/**
* Deletes a register record by the provided hash and returns the success of the database deletion
*
* @param string $hash
* @param string $hash
* @return boolean
* @throws \Exception
*/
public static function deleteByHash($hash)
{

32
src/Model/Search.php Normal file
View file

@ -0,0 +1,32 @@
<?php
namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Database\DBA;
/**
* Model for DB specific logic for the search entity
*/
class Search extends BaseObject
{
/**
* Returns the list of user defined tags (e.g. #Friendica)
*
* @return array
*
* @throws \Exception
*/
public static function getUserTags()
{
$termsStmt = DBA::p("SELECT DISTINCT(`term`) FROM `search`");
$tags = [];
while ($term = DBA::fetch($termsStmt)) {
$tags[] = trim($term['term'], '#');
}
return $tags;
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* @file src/Model/Storage/Filesystem.php
* @brief Storage backend system
*/
namespace Friendica\Model\Storage;
use Friendica\Core\Logger;
use Friendica\Core\L10n;
use Friendica\Database\DBA;
/**
* @brief Database based storage system
*
* This class manage data stored in database table.
*/
class Database implements IStorage
{
public static function get($ref)
{
$r = DBA::selectFirst('storage', ['data'], ['id' => $ref]);
if (!DBA::isResult($r)) {
return '';
}
return $r['data'];
}
public static function put($data, $ref = '')
{
if ($ref !== '') {
$r = DBA::update('storage', ['data' => $data], ['id' => $ref]);
if ($r === false) {
Logger::log('Failed to update data with id ' . $ref . ': ' . DBA::errorNo() . ' : ' . DBA::errorMessage());
throw new StorageException(L10n::t('Database storage failed to update %s', $ref));
}
return $ref;
} else {
$r = DBA::insert('storage', ['data' => $data]);
if ($r === false) {
Logger::log('Failed to insert data: ' . DBA::errorNo() . ' : ' . DBA::errorMessage());
throw new StorageException(L10n::t('Database storage failed to insert data'));
}
return DBA::lastInsertId();
}
}
public static function delete($ref)
{
return DBA::delete('storage', ['id' => $ref]);
}
public static function getOptions()
{
return [];
}
public static function saveOptions($data)
{
return [];
}
}

View file

@ -0,0 +1,145 @@
<?php
/**
* @file src/Model/Storage/Filesystem.php
* @brief Storage backend system
*/
namespace Friendica\Model\Storage;
use Friendica\Core\Config;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Util\Strings;
/**
* @brief Filesystem based storage backend
*
* This class manage data on filesystem.
* Base folder for storage is set in storage.filesystem_path.
* Best would be for storage folder to be outside webserver folder, we are using a
* folder relative to code tree root as default to ease things for users in shared hostings.
* Each new resource gets a value as reference and is saved in a
* folder tree stucture created from that value.
*/
class Filesystem implements IStorage
{
// Default base folder
const DEFAULT_BASE_FOLDER = 'storage';
private static function getBasePath()
{
$path = Config::get('storage', 'filesystem_path', self::DEFAULT_BASE_FOLDER);
return rtrim($path, '/');
}
/**
* @brief Split data ref and return file path
* @param string $ref Data reference
* @return string
*/
private static function pathForRef($ref)
{
$base = self::getBasePath();
$fold1 = substr($ref, 0, 2);
$fold2 = substr($ref, 2, 2);
$file = substr($ref, 4);
return implode('/', [$base, $fold1, $fold2, $file]);
}
/**
* @brief Create dirctory tree to store file, with .htaccess and index.html files
* @param string $file Path and filename
* @throws StorageException
*/
private static function createFoldersForFile($file)
{
$path = dirname($file);
if (!is_dir($path)) {
if (!mkdir($path, 0770, true)) {
Logger::log('Failed to create dirs ' . $path);
throw new StorageException(L10n::t('Filesystem storage failed to create "%s". Check you write permissions.', $path));
}
}
$base = self::getBasePath();
while ($path !== $base) {
if (!is_file($path . '/index.html')) {
file_put_contents($path . '/index.html', '');
}
chmod($path . '/index.html', 0660);
chmod($path, 0770);
$path = dirname($path);
}
if (!is_file($path . '/index.html')) {
file_put_contents($path . '/index.html', '');
chmod($path . '/index.html', 0660);
}
}
public static function get($ref)
{
$file = self::pathForRef($ref);
if (!is_file($file)) {
return '';
}
return file_get_contents($file);
}
public static function put($data, $ref = '')
{
if ($ref === '') {
$ref = Strings::getRandomHex();
}
$file = self::pathForRef($ref);
self::createFoldersForFile($file);
$r = file_put_contents($file, $data);
if ($r === FALSE) {
Logger::log('Failed to write data to ' . $file);
throw new StorageException(L10n::t('Filesystem storage failed to save data to "%s". Check your write permissions', $file));
}
chmod($file, 0660);
return $ref;
}
public static function delete($ref)
{
$file = self::pathForRef($ref);
// return true if file doesn't exists. we want to delete it: success with zero work!
if (!is_file($file)) {
return true;
}
return unlink($file);
}
public static function getOptions()
{
return [
'storagepath' => [
'input',
L10n::t('Storage base path'),
self::getBasePath(),
L10n::t('Folder where uploaded files are saved. For maximum security, This should be a path outside web server folder tree')
]
];
}
public static function saveOptions($data)
{
$storagepath = defaults($data, 'storagepath', '');
if ($storagepath === '' || !is_dir($storagepath)) {
return [
'storagepath' => L10n::t('Enter a valid existing folder')
];
};
Config::set('storage', 'filesystem_path', $storagepath);
return [];
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* @file src/Model/Storage/IStorage.php
* @brief Storage backend system
*/
namespace Friendica\Model\Storage;
/**
* @brief Interface for storage backends
*/
interface IStorage
{
/**
* @brief Get data from backend
* @param string $ref Data reference
* @return string
*/
public static function get($ref);
/**
* @brief Put data in backend as $ref. If $ref is not defined a new reference is created.
* @param string $data Data to save
* @param string $ref Data referece. Optional.
* @return string Saved data referece
*/
public static function put($data, $ref = "");
/**
* @brief Remove data from backend
* @param string $ref Data referece
* @return boolean True on success
*/
public static function delete($ref);
/**
* @brief Get info about storage options
*
* @return array
*
* This method return an array with informations about storage options
* from which the form presented to the user is build.
*
* The returned array is:
*
* [
* 'option1name' => [ ..info.. ],
* 'option2name' => [ ..info.. ],
* ...
* ]
*
* An empty array can be returned if backend doesn't have any options
*
* The info array for each option MUST be as follows:
*
* [
* 'type', // define the field used in form, and the type of data.
* // one of 'checkbox', 'combobox', 'custom', 'datetime',
* // 'input', 'intcheckbox', 'password', 'radio', 'richtext'
* // 'select', 'select_raw', 'textarea', 'yesno'
*
* 'label', // Translatable label of the field
* 'value', // Current value
* 'help text', // Translatable description for the field
* extra data // Optional. Depends on 'type':
* // select: array [ value => label ] of choices
* // intcheckbox: value of input element
* // select_raw: prebuild html string of < option > tags
* // yesno: array [ 'label no', 'label yes']
* ]
*
* See https://github.com/friendica/friendica/wiki/Quick-Template-Guide
*/
public static function getOptions();
/**
* @brief Validate and save options
*
* @param array $data Array [optionname => value] to be saved
*
* @return array Validation errors: [optionname => error message]
*
* Return array must be empty if no error.
*/
public static function saveOptions($data);
}

View file

@ -0,0 +1,14 @@
<?php
/**
* @file src/Model/Storage/StorageException.php
* @brief Storage backend system
*/
namespace Friendica\Model\Storage;
/**
* @brief Storage Exception
*/
class StorageException extends \Exception
{
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file src/Model/Storage/SystemStorage.php
* @brief Storage backend system
*/
namespace Friendica\Model\Storage;
use \BadMethodCallException;
/**
* @brief System resource storage class
*
* This class is used to load system resources, like images.
* Is not intended to be selectable by admins as default storage class.
*/
class SystemResource implements IStorage
{
// Valid folders to look for resources
const VALID_FOLDERS = ["images"];
public static function get($filename)
{
$folder = dirname($filename);
if (!in_array($folder, self::VALID_FOLDERS)) {
return "";
}
if (!file_exists($filename)) {
return "";
}
return file_get_contents($filename);
}
public static function put($data, $filename = "")
{
throw new BadMethodCallException();
}
public static function delete($filename)
{
throw new BadMethodCallException();
}
public static function getOptions()
{
return [];
}
public static function saveOptions($data)
{
return [];
}
}

View file

@ -1,41 +1,171 @@
<?php
/**
* @file src/Model/Term
* @file src/Model/Term.php
*/
namespace Friendica\Model;
use Friendica\Core\Cache;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Util\Strings;
require_once 'boot.php';
require_once 'include/conversation.php';
require_once 'include/dba.php';
/**
* Class Term
*
* This Model class handles term table interactions.
* This tables stores relevant terms related to posts, photos and searches, like hashtags, mentions and
* user-applied categories.
*
* @package Friendica\Model
*/
class Term
{
public static function tagTextFromItemId($itemid)
{
$tag_text = '';
$condition = ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => [TERM_HASHTAG, TERM_MENTION]];
$tags = DBA::select('term', [], $condition);
while ($tag = DBA::fetch($tags)) {
if ($tag_text != '') {
$tag_text .= ',';
}
const UNKNOWN = 0;
const HASHTAG = 1;
const MENTION = 2;
const CATEGORY = 3;
const PCATEGORY = 4;
const FILE = 5;
const SAVEDSEARCH = 6;
const CONVERSATION = 7;
/**
* An implicit mention is a mention in a comment body that is redundant with the threading information.
*/
const IMPLICIT_MENTION = 8;
/**
* An exclusive mention transfers the ownership of the post to the target account, usually a forum.
*/
const EXCLUSIVE_MENTION = 9;
if ($tag['type'] == 1) {
$tag_text .= '#';
} else {
$tag_text .= '@';
const TAG_CHARACTER = [
self::HASHTAG => '#',
self::MENTION => '@',
self::IMPLICIT_MENTION => '%',
self::EXCLUSIVE_MENTION => '!',
];
const OBJECT_TYPE_POST = 1;
const OBJECT_TYPE_PHOTO = 2;
/**
* Returns a list of the most frequent global hashtags over the given period
*
* @param int $period Period in hours to consider posts
* @return array
* @throws \Exception
*/
public static function getGlobalTrendingHashtags(int $period, $limit = 10)
{
$tags = Cache::get('global_trending_tags');
if (!$tags) {
$tagsStmt = DBA::p("SELECT t.`term`, COUNT(*) AS `score`
FROM `term` t
JOIN `item` i ON i.`id` = t.`oid` AND i.`uid` = t.`uid`
JOIN `thread` ON `thread`.`iid` = i.`id`
WHERE `thread`.`visible`
AND NOT `thread`.`deleted`
AND NOT `thread`.`moderated`
AND NOT `thread`.`private`
AND t.`uid` = 0
AND t.`otype` = ?
AND t.`type` = ?
AND t.`term` != ''
AND i.`received` > DATE_SUB(NOW(), INTERVAL ? HOUR)
GROUP BY `term`
ORDER BY `score` DESC
LIMIT ?",
Term::OBJECT_TYPE_POST,
Term::HASHTAG,
$period,
$limit
);
if (DBA::isResult($tagsStmt)) {
$tags = DBA::toArray($tagsStmt);
Cache::set('global_trending_tags', $tags, Cache::HOUR);
}
$tag_text .= '[url=' . $tag['url'] . ']' . $tag['term'] . '[/url]';
}
return $tag_text;
return $tags ?: [];
}
public static function tagArrayFromItemId($itemid, $type = [TERM_HASHTAG, TERM_MENTION])
/**
* Returns a list of the most frequent local hashtags over the given period
*
* @param int $period Period in hours to consider posts
* @return array
* @throws \Exception
*/
public static function getLocalTrendingHashtags(int $period, $limit = 10)
{
$condition = ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => $type];
$tags = Cache::get('local_trending_tags');
if (!$tags) {
$tagsStmt = DBA::p("SELECT t.`term`, COUNT(*) AS `score`
FROM `term` t
JOIN `item` i ON i.`id` = t.`oid` AND i.`uid` = t.`uid`
JOIN `thread` ON `thread`.`iid` = i.`id`
JOIN `user` ON `user`.`uid` = `thread`.`uid` AND NOT `user`.`hidewall`
WHERE `thread`.`visible`
AND NOT `thread`.`deleted`
AND NOT `thread`.`moderated`
AND NOT `thread`.`private`
AND `thread`.`wall`
AND `thread`.`origin`
AND t.`otype` = ?
AND t.`type` = ?
AND t.`term` != ''
AND i.`received` > DATE_SUB(NOW(), INTERVAL ? HOUR)
GROUP BY `term`
ORDER BY `score` DESC
LIMIT ?",
Term::OBJECT_TYPE_POST,
Term::HASHTAG,
$period,
$limit
);
if (DBA::isResult($tagsStmt)) {
$tags = DBA::toArray($tagsStmt);
Cache::set('local_trending_tags', $tags, Cache::HOUR);
}
}
return $tags ?: [];
}
/**
* Generates the legacy item.tag field comma-separated BBCode string from an item ID.
* Includes only hashtags, implicit and explicit mentions.
*
* @param int $item_id
* @return string
* @throws \Exception
*/
public static function tagTextFromItemId($item_id)
{
$tag_list = [];
$tags = self::tagArrayFromItemId($item_id, [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION]);
foreach ($tags as $tag) {
$tag_list[] = self::TAG_CHARACTER[$tag['type']] . '[url=' . $tag['url'] . ']' . $tag['term'] . '[/url]';
}
return implode(',', $tag_list);
}
/**
* Retrieves the terms from the provided type(s) associated with the provided item ID.
*
* @param int $item_id
* @param int|array $type
* @return array
* @throws \Exception
*/
public static function tagArrayFromItemId($item_id, $type = [self::HASHTAG, self::MENTION])
{
$condition = ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => $type];
$tags = DBA::select('term', ['type', 'term', 'url'], $condition);
if (!DBA::isResult($tags)) {
return [];
@ -44,22 +174,39 @@ class Term
return DBA::toArray($tags);
}
public static function fileTextFromItemId($itemid)
/**
* Generates the legacy item.file field string from an item ID.
* Includes only file and category terms.
*
* @param int $item_id
* @return string
* @throws \Exception
*/
public static function fileTextFromItemId($item_id)
{
$file_text = '';
$condition = ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => [TERM_FILE, TERM_CATEGORY]];
$tags = DBA::select('term', [], $condition);
while ($tag = DBA::fetch($tags)) {
if ($tag['type'] == TERM_CATEGORY) {
$tags = self::tagArrayFromItemId($item_id, [self::FILE, self::CATEGORY]);
foreach ($tags as $tag) {
if ($tag['type'] == self::CATEGORY) {
$file_text .= '<' . $tag['term'] . '>';
} else {
$file_text .= '[' . $tag['term'] . ']';
}
}
return $file_text;
}
public static function insertFromTagFieldByItemId($itemid, $tags)
/**
* Inserts new terms for the provided item ID based on the legacy item.tag field BBCode content.
* Deletes all previous tag terms for the same item ID.
* Sets both the item.mention and thread.mentions field flags if a mention concerning the item UID is found.
*
* @param int $item_id
* @param string $tag_str
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function insertFromTagFieldByItemId($item_id, $tag_str)
{
$profile_base = System::baseUrl();
$profile_data = parse_url($profile_base);
@ -68,32 +215,32 @@ class Term
$profile_base_diaspora = $profile_data['host'] . $profile_path . '/u/';
$fields = ['guid', 'uid', 'id', 'edited', 'deleted', 'created', 'received', 'title', 'body', 'parent'];
$message = Item::selectFirst($fields, ['id' => $itemid]);
if (!DBA::isResult($message)) {
$item = Item::selectFirst($fields, ['id' => $item_id]);
if (!DBA::isResult($item)) {
return;
}
$message['tag'] = $tags;
$item['tag'] = $tag_str;
// Clean up all tags
self::deleteByItemId($itemid);
self::deleteByItemId($item_id);
if ($message['deleted']) {
if ($item['deleted']) {
return;
}
$taglist = explode(',', $message['tag']);
$taglist = explode(',', $item['tag']);
$tags_string = '';
foreach ($taglist as $tag) {
if ((substr(trim($tag), 0, 1) == '#') || (substr(trim($tag), 0, 1) == '@')) {
if (Strings::startsWith($tag, self::TAG_CHARACTER)) {
$tags_string .= ' ' . trim($tag);
} else {
$tags_string .= ' #' . trim($tag);
}
}
$data = ' ' . $message['title'] . ' ' . $message['body'] . ' ' . $tags_string . ' ';
$data = ' ' . $item['title'] . ' ' . $item['body'] . ' ' . $tags_string . ' ';
// ignore anything in a code block
$data = preg_replace('/\[code\](.*?)\[\/code\]/sm', '', $data);
@ -107,11 +254,15 @@ class Term
}
}
$pattern = '/\W([\#@])\[url\=(.*?)\](.*?)\[\/url\]/ism';
$pattern = '/\W([\#@!%])\[url\=(.*?)\](.*?)\[\/url\]/ism';
if (preg_match_all($pattern, $data, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
if ($match[1] == '@') {
if (in_array($match[1], [
self::TAG_CHARACTER[self::MENTION],
self::TAG_CHARACTER[self::IMPLICIT_MENTION],
self::TAG_CHARACTER[self::EXCLUSIVE_MENTION]
])) {
$contact = Contact::getDetailsByURL($match[2], 0);
if (!empty($contact['addr'])) {
$match[3] = $contact['addr'];
@ -122,12 +273,12 @@ class Term
}
}
$tags[$match[1] . trim($match[3], ',.:;[]/\"?!')] = $match[2];
$tags[$match[2]] = $match[1] . trim($match[3], ',.:;[]/\"?!');
}
}
foreach ($tags as $tag => $link) {
if (substr(trim($tag), 0, 1) == '#') {
foreach ($tags as $link => $tag) {
if (self::isType($tag, self::HASHTAG)) {
// try to ignore #039 or #1 or anything like that
if (ctype_digit(substr(trim($tag), 1))) {
continue;
@ -138,10 +289,15 @@ class Term
continue;
}
$type = TERM_HASHTAG;
$type = self::HASHTAG;
$term = substr($tag, 1);
} elseif (substr(trim($tag), 0, 1) == '@') {
$type = TERM_MENTION;
$link = '';
} elseif (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION)) {
if (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION)) {
$type = self::MENTION;
} else {
$type = self::IMPLICIT_MENTION;
}
$contact = Contact::getDetailsByURL($link, 0);
if (!empty($contact['name'])) {
@ -150,42 +306,51 @@ class Term
$term = substr($tag, 1);
}
} else { // This shouldn't happen
$type = TERM_HASHTAG;
$type = self::HASHTAG;
$term = $tag;
$link = '';
Logger::notice('Unknown term type', ['tag' => $tag]);
}
if (DBA::exists('term', ['uid' => $message['uid'], 'otype' => TERM_OBJ_POST, 'oid' => $itemid, 'url' => $link])) {
if (DBA::exists('term', ['uid' => $item['uid'], 'otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'term' => $term, 'type' => $type])) {
continue;
}
if ($message['uid'] == 0) {
if ($item['uid'] == 0) {
$global = true;
DBA::update('term', ['global' => true], ['otype' => TERM_OBJ_POST, 'guid' => $message['guid']]);
DBA::update('term', ['global' => true], ['otype' => self::OBJECT_TYPE_POST, 'guid' => $item['guid']]);
} else {
$global = DBA::exists('term', ['uid' => 0, 'otype' => TERM_OBJ_POST, 'guid' => $message['guid']]);
$global = DBA::exists('term', ['uid' => 0, 'otype' => self::OBJECT_TYPE_POST, 'guid' => $item['guid']]);
}
DBA::insert('term', [
'uid' => $message['uid'],
'oid' => $itemid,
'otype' => TERM_OBJ_POST,
'uid' => $item['uid'],
'oid' => $item_id,
'otype' => self::OBJECT_TYPE_POST,
'type' => $type,
'term' => $term,
'url' => $link,
'guid' => $message['guid'],
'created' => $message['created'],
'received' => $message['received'],
'guid' => $item['guid'],
'created' => $item['created'],
'received' => $item['received'],
'global' => $global
]);
// Search for mentions
if ((substr($tag, 0, 1) == '@') && (strpos($link, $profile_base_friendica) || strpos($link, $profile_base_diaspora))) {
$users = q("SELECT `uid` FROM `contact` WHERE self AND (`url` = '%s' OR `nurl` = '%s')", $link, $link);
if (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION)
&& (
strpos($link, $profile_base_friendica) !== false
|| strpos($link, $profile_base_diaspora) !== false
)
) {
$users_stmt = DBA::p("SELECT `uid` FROM `contact` WHERE self AND (`url` = ? OR `nurl` = ?)", $link, $link);
$users = DBA::toArray($users_stmt);
foreach ($users AS $user) {
if ($user['uid'] == $message['uid']) {
/// @todo This function is called frim Item::update - so we mustn't call that function here
DBA::update('item', ['mention' => true], ['id' => $itemid]);
DBA::update('thread', ['mention' => true], ['iid' => $message['parent']]);
if ($user['uid'] == $item['uid']) {
/// @todo This function is called from Item::update - so we mustn't call that function here
DBA::update('item', ['mention' => true], ['id' => $item_id]);
DBA::update('thread', ['mention' => true], ['iid' => $item['parent']]);
}
}
}
@ -193,18 +358,23 @@ class Term
}
/**
* @param integer $itemid item id
* Inserts new terms for the provided item ID based on the legacy item.file field BBCode content.
* Deletes all previous file terms for the same item ID.
*
* @param integer $item_id item id
* @param $files
* @return void
* @throws \Exception
*/
public static function insertFromFileFieldByItemId($itemid, $files)
public static function insertFromFileFieldByItemId($item_id, $files)
{
$message = Item::selectFirst(['uid', 'deleted'], ['id' => $itemid]);
$message = Item::selectFirst(['uid', 'deleted'], ['id' => $item_id]);
if (!DBA::isResult($message)) {
return;
}
// Clean up all tags
DBA::delete('term', ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => [TERM_FILE, TERM_CATEGORY]]);
DBA::delete('term', ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => [self::FILE, self::CATEGORY]]);
if ($message["deleted"]) {
return;
@ -216,9 +386,9 @@ class Term
foreach ($files[1] as $file) {
DBA::insert('term', [
'uid' => $message["uid"],
'oid' => $itemid,
'otype' => TERM_OBJ_POST,
'type' => TERM_FILE,
'oid' => $item_id,
'otype' => self::OBJECT_TYPE_POST,
'type' => self::FILE,
'term' => $file
]);
}
@ -228,9 +398,9 @@ class Term
foreach ($files[1] as $file) {
DBA::insert('term', [
'uid' => $message["uid"],
'oid' => $itemid,
'otype' => TERM_OBJ_POST,
'type' => TERM_CATEGORY,
'oid' => $item_id,
'otype' => self::OBJECT_TYPE_POST,
'type' => self::CATEGORY,
'term' => $file
]);
}
@ -243,6 +413,8 @@ class Term
*
* @param array $item
* @return array
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function populateTagsFromItem(&$item)
{
@ -250,6 +422,7 @@ class Term
'tags' => [],
'hashtags' => [],
'mentions' => [],
'implicit_mentions' => [],
];
$searchpath = System::baseUrl() . "/search?tag=";
@ -257,34 +430,35 @@ class Term
$taglist = DBA::select(
'term',
['type', 'term', 'url'],
["`otype` = ? AND `oid` = ? AND `type` IN (?, ?)", TERM_OBJ_POST, $item['id'], TERM_HASHTAG, TERM_MENTION],
['otype' => self::OBJECT_TYPE_POST, 'oid' => $item['id'], 'type' => [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION]],
['order' => ['tid']]
);
while ($tag = DBA::fetch($taglist)) {
if ($tag["url"] == "") {
$tag["url"] = $searchpath . $tag["term"];
if ($tag['url'] == '') {
$tag['url'] = $searchpath . rawurlencode($tag['term']);
}
$orig_tag = $tag["url"];
$orig_tag = $tag['url'];
$author = ['uid' => 0, 'id' => $item['author-id'],
'network' => $item['author-network'], 'url' => $item['author-link']];
$tag["url"] = Contact::magicLinkByContact($author, $tag['url']);
$prefix = self::TAG_CHARACTER[$tag['type']];
switch($tag['type']) {
case self::HASHTAG:
if ($orig_tag != $tag['url']) {
$item['body'] = str_replace($orig_tag, $tag['url'], $item['body']);
}
if ($tag["type"] == TERM_HASHTAG) {
if ($orig_tag != $tag["url"]) {
$item['body'] = str_replace($orig_tag, $tag["url"], $item['body']);
}
$return['hashtags'][] = "#<a href=\"" . $tag["url"] . "\" target=\"_blank\">" . $tag["term"] . "</a>";
$prefix = "#";
} elseif ($tag["type"] == TERM_MENTION) {
$return['mentions'][] = "@<a href=\"" . $tag["url"] . "\" target=\"_blank\">" . $tag["term"] . "</a>";
$prefix = "@";
$return['hashtags'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank">' . $tag['term'] . '</a>';
$return['tags'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank">' . $tag['term'] . '</a>';
break;
case self::MENTION:
$tag['url'] = Contact::magicLink($tag['url']);
$return['mentions'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank">' . $tag['term'] . '</a>';
$return['tags'][] = $prefix . '<a href="' . $tag['url'] . '" target="_blank">' . $tag['term'] . '</a>';
break;
case self::IMPLICIT_MENTION:
$return['implicit_mentions'][] = $prefix . $tag['term'];
break;
}
$return['tags'][] = $prefix . "<a href=\"" . $tag["url"] . "\" target=\"_blank\">" . $tag["term"] . "</a>";
}
DBA::close($taglist);
@ -292,18 +466,38 @@ class Term
}
/**
* Delete all tags from an item
* @param int itemid - choose from which item the tags will be removed
* @param array type - items type. default is [TERM_HASHTAG, TERM_MENTION]
* Delete tags of the specific type(s) from an item
*
* @param int $item_id
* @param int|array $type
* @throws \Exception
*/
public static function deleteByItemId($itemid, $type = [TERM_HASHTAG, TERM_MENTION])
public static function deleteByItemId($item_id, $type = [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION])
{
if (empty($itemid)) {
if (empty($item_id)) {
return;
}
// Clean up all tags
DBA::delete('term', ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => $type]);
DBA::delete('term', ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => $type]);
}
/**
* Check if the provided tag is of one of the provided term types.
*
* @param string $tag
* @param int ...$types
* @return bool
*/
public static function isType($tag, ...$types)
{
$tag_chars = [];
foreach ($types as $type) {
if (array_key_exists($type, self::TAG_CHARACTER)) {
$tag_chars[] = self::TAG_CHARACTER[$type];
}
}
return Strings::startsWith($tag, $tag_chars);
}
}

View file

@ -0,0 +1,137 @@
<?php
namespace Friendica\Model\TwoFactor;
use Friendica\BaseObject;
use Friendica\Database\DBA;
use Friendica\Model\User;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Temporal;
use PragmaRX\Random\Random;
/**
* Manages users' two-factor recovery hashed_passwords in the 2fa_app_specific_passwords table
*
* @package Friendica\Model
*/
class AppSpecificPassword extends BaseObject
{
public static function countForUser($uid)
{
return DBA::count('2fa_app_specific_password', ['uid' => $uid]);
}
public static function checkDuplicateForUser($uid, $description)
{
return DBA::exists('2fa_app_specific_password', ['uid' => $uid, 'description' => $description]);
}
/**
* Checks the provided hashed_password is available to use for login by the provided user
*
* @param int $uid User ID
* @param string $plaintextPassword
* @return bool
* @throws \Exception
*/
public static function authenticateUser($uid, $plaintextPassword)
{
$appSpecificPasswords = self::getListForUser($uid);
$return = false;
foreach ($appSpecificPasswords as $appSpecificPassword) {
if (password_verify($plaintextPassword, $appSpecificPassword['hashed_password'])) {
$fields = ['last_used' => DateTimeFormat::utcNow()];
if (password_needs_rehash($appSpecificPassword['hashed_password'], PASSWORD_DEFAULT)) {
$fields['hashed_password'] = User::hashPassword($plaintextPassword);
}
self::update($appSpecificPassword['id'], $fields);
$return |= true;
}
}
return $return;
}
/**
* Returns a complete list of all recovery hashed_passwords for the provided user, including the used status
*
* @param int $uid User ID
* @return array
* @throws \Exception
*/
public static function getListForUser($uid)
{
$appSpecificPasswordsStmt = DBA::select('2fa_app_specific_password', ['id', 'description', 'hashed_password', 'last_used'], ['uid' => $uid]);
$appSpecificPasswords = DBA::toArray($appSpecificPasswordsStmt);
array_walk($appSpecificPasswords, function (&$value) {
$value['ago'] = Temporal::getRelativeDate($value['last_used']);
});
return $appSpecificPasswords;
}
/**
* Generates a new app specific password for the provided user and hashes it in the database.
*
* @param int $uid User ID
* @param string $description Password description
* @return array The new app-specific password data structure with the plaintext password added
* @throws \Exception
*/
public static function generateForUser(int $uid, $description)
{
$Random = (new Random())->size(40);
$plaintextPassword = $Random->get();
$generated = DateTimeFormat::utcNow();
$fields = [
'uid' => $uid,
'description' => $description,
'hashed_password' => User::hashPassword($plaintextPassword),
'generated' => $generated,
];
DBA::insert('2fa_app_specific_password', $fields);
$fields['id'] = DBA::lastInsertId();
$fields['plaintext_password'] = $plaintextPassword;
return $fields;
}
private static function update($appSpecificPasswordId, $fields)
{
return DBA::update('2fa_app_specific_password', $fields, ['id' => $appSpecificPasswordId]);
}
/**
* Deletes all the recovery hashed_passwords for the provided user.
*
* @param int $uid User ID
* @return bool
* @throws \Exception
*/
public static function deleteAllForUser(int $uid)
{
return DBA::delete('2fa_app_specific_password', ['uid' => $uid]);
}
/**
* @param int $uid
* @param int $app_specific_password_id
* @return bool
* @throws \Exception
*/
public static function deleteForUser(int $uid, int $app_specific_password_id)
{
return DBA::delete('2fa_app_specific_password', ['id' => $app_specific_password_id, 'uid' => $uid]);
}
}

View file

@ -0,0 +1,125 @@
<?php
namespace Friendica\Model\TwoFactor;
use Friendica\BaseObject;
use Friendica\Database\DBA;
use Friendica\Util\DateTimeFormat;
use PragmaRX\Random\Random;
use PragmaRX\Recovery\Recovery;
/**
* Manages users' two-factor recovery codes in the 2fa_recovery_codes table
*
* @package Friendica\Model
*/
class RecoveryCode extends BaseObject
{
/**
* Returns the number of code the provided users can still use to replace a TOTP code
*
* @param int $uid User ID
* @return int
* @throws \Exception
*/
public static function countValidForUser($uid)
{
return DBA::count('2fa_recovery_codes', ['uid' => $uid, 'used' => null]);
}
/**
* Checks the provided code is available to use for login by the provided user
*
* @param int $uid User ID
* @param string $code
* @return bool
* @throws \Exception
*/
public static function existsForUser($uid, $code)
{
return DBA::exists('2fa_recovery_codes', ['uid' => $uid, 'code' => $code, 'used' => null]);
}
/**
* Returns a complete list of all recovery codes for the provided user, including the used status
*
* @param int $uid User ID
* @return array
* @throws \Exception
*/
public static function getListForUser($uid)
{
$codesStmt = DBA::select('2fa_recovery_codes', ['code', 'used'], ['uid' => $uid]);
return DBA::toArray($codesStmt);
}
/**
* Marks the provided code as used for the provided user.
* Returns false if the code doesn't exist for the user or it has been used already.
*
* @param int $uid User ID
* @param string $code
* @return bool
* @throws \Exception
*/
public static function markUsedForUser($uid, $code)
{
DBA::update('2fa_recovery_codes', ['used' => DateTimeFormat::utcNow()], ['uid' => $uid, 'code' => $code, 'used' => null]);
return DBA::affectedRows() > 0;
}
/**
* Generates a fresh set of recovery codes for the provided user.
* Generates 12 codes constituted of 2 blocks of 6 characters separated by a dash.
*
* @param int $uid User ID
* @throws \Exception
*/
public static function generateForUser($uid)
{
$Random = (new Random())->pattern('[a-z0-9]');
$RecoveryGenerator = new Recovery($Random);
$codes = $RecoveryGenerator
->setCount(12)
->setBlocks(2)
->setChars(6)
->lowercase(true)
->toArray();
$generated = DateTimeFormat::utcNow();
foreach ($codes as $code) {
DBA::insert('2fa_recovery_codes', [
'uid' => $uid,
'code' => $code,
'generated' => $generated
]);
}
}
/**
* Deletes all the recovery codes for the provided user.
*
* @param int $uid User ID
* @throws \Exception
*/
public static function deleteForUser($uid)
{
DBA::delete('2fa_recovery_codes', ['uid' => $uid]);
}
/**
* Replaces the existing recovery codes for the provided user by a freshly generated set.
*
* @param int $uid User ID
* @throws \Exception
*/
public static function regenerateForUser($uid)
{
self::deleteForUser($uid);
self::generateForUser($uid);
}
}

View file

@ -1,40 +1,93 @@
<?php
/**
* @file src/Model/User.php
* @brief This file includes the User class with user related database functions
*/
namespace Friendica\Model;
use DivineOmega\PasswordExposed;
use Exception;
use Friendica\Core\Addon;
use Friendica\Core\Config;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\PConfig;
use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\Model\Photo;
use Friendica\Model\TwoFactor\AppSpecificPassword;
use Friendica\Object\Image;
use Friendica\Util\Crypto;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network;
use Friendica\Util\Strings;
use Friendica\Worker\Delivery;
use LightOpenID;
require_once 'boot.php';
require_once 'include/dba.php';
require_once 'include/enotify.php';
require_once 'include/text.php';
/**
* @brief This class handles User related functions
*/
class User
{
/**
* Page/profile types
*
* PAGE_FLAGS_NORMAL is a typical personal profile account
* PAGE_FLAGS_SOAPBOX automatically approves all friend requests as Contact::SHARING, (readonly)
* PAGE_FLAGS_COMMUNITY automatically approves all friend requests as Contact::SHARING, but with
* write access to wall and comments (no email and not included in page owner's ACL lists)
* PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends (Contact::FRIEND).
*
* @{
*/
const PAGE_FLAGS_NORMAL = 0;
const PAGE_FLAGS_SOAPBOX = 1;
const PAGE_FLAGS_COMMUNITY = 2;
const PAGE_FLAGS_FREELOVE = 3;
const PAGE_FLAGS_BLOG = 4;
const PAGE_FLAGS_PRVGROUP = 5;
/**
* @}
*/
/**
* Account types
*
* ACCOUNT_TYPE_PERSON - the account belongs to a person
* Associated page types: PAGE_FLAGS_NORMAL, PAGE_FLAGS_SOAPBOX, PAGE_FLAGS_FREELOVE
*
* ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation
* Associated page type: PAGE_FLAGS_SOAPBOX
*
* ACCOUNT_TYPE_NEWS - the account is a news reflector
* Associated page type: PAGE_FLAGS_SOAPBOX
*
* ACCOUNT_TYPE_COMMUNITY - the account is community forum
* Associated page types: PAGE_COMMUNITY, PAGE_FLAGS_PRVGROUP
*
* ACCOUNT_TYPE_RELAY - the account is a relay
* This will only be assigned to contacts, not to user accounts
* @{
*/
const ACCOUNT_TYPE_PERSON = 0;
const ACCOUNT_TYPE_ORGANISATION = 1;
const ACCOUNT_TYPE_NEWS = 2;
const ACCOUNT_TYPE_COMMUNITY = 3;
const ACCOUNT_TYPE_RELAY = 4;
/**
* @}
*/
/**
* Returns true if a user record exists with the provided id
*
* @param integer $uid
* @return boolean
* @throws Exception
*/
public static function exists($uid)
{
@ -43,11 +96,24 @@ class User
/**
* @param integer $uid
* @param array $fields
* @return array|boolean User record if it exists, false otherwise
* @throws Exception
*/
public static function getById($uid)
public static function getById($uid, array $fields = [])
{
return DBA::selectFirst('user', [], ['uid' => $uid]);
return DBA::selectFirst('user', $fields, ['uid' => $uid]);
}
/**
* @param string $nickname
* @param array $fields
* @return array|boolean User record if it exists, false otherwise
* @throws Exception
*/
public static function getByNickname($nickname, array $fields = [])
{
return DBA::selectFirst('user', $fields, ['nickname' => $nickname]);
}
/**
@ -56,10 +122,11 @@ class User
* @param string $url
*
* @return integer user id
* @throws Exception
*/
public static function getIdForURL($url)
{
$self = DBA::selectFirst('contact', ['uid'], ['nurl' => normalise_link($url), 'self' => true]);
$self = DBA::selectFirst('contact', ['uid'], ['nurl' => Strings::normaliseLink($url), 'self' => true]);
if (!DBA::isResult($self)) {
return false;
} else {
@ -67,14 +134,33 @@ class User
}
}
/**
* Get a user based on its email
*
* @param string $email
* @param array $fields
*
* @return array|boolean User record if it exists, false otherwise
*
* @throws Exception
*/
public static function getByEmail($email, array $fields = [])
{
return DBA::selectFirst('user', $fields, ['email' => $email]);
}
/**
* @brief Get owner data by user id
*
* @param int $uid
* @param boolean $check_valid Test if data is invalid and correct it
* @return boolean|array
* @throws Exception
*/
public static function getOwnerDataById($uid) {
$r = DBA::fetchFirst("SELECT
public static function getOwnerDataById($uid, $check_valid = true)
{
$r = DBA::fetchFirst(
"SELECT
`contact`.*,
`user`.`prvkey` AS `uprvkey`,
`user`.`timezone`,
@ -83,7 +169,8 @@ class User
`user`.`spubkey`,
`user`.`page-flags`,
`user`.`account-type`,
`user`.`prvnets`
`user`.`prvnets`,
`user`.`account_removed`
FROM `contact`
INNER JOIN `user`
ON `user`.`uid` = `contact`.`uid`
@ -95,6 +182,41 @@ class User
if (!DBA::isResult($r)) {
return false;
}
if (empty($r['nickname'])) {
return false;
}
if (!$check_valid) {
return $r;
}
// Check if the returned data is valid, otherwise fix it. See issue #6122
// Check for correct url and normalised nurl
$url = System::baseUrl() . '/profile/' . $r['nickname'];
$repair = ($r['url'] != $url) || ($r['nurl'] != Strings::normaliseLink($r['url']));
if (!$repair) {
// Check if "addr" is present and correct
$addr = $r['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);
$repair = ($addr != $r['addr']);
}
if (!$repair) {
// Check if the avatar field is filled and the photo directs to the correct path
$avatar = Photo::selectFirst(['resource-id'], ['uid' => $uid, 'profile' => true]);
if (DBA::isResult($avatar)) {
$repair = empty($r['avatar']) || !strpos($r['photo'], $avatar['resource-id']);
}
}
if ($repair) {
Contact::updateSelfFromUserID($uid);
// Return the corrected data and avoid a loop
$r = self::getOwnerDataById($uid, false);
}
return $r;
}
@ -103,6 +225,7 @@ class User
*
* @param int $nick
* @return boolean|array
* @throws Exception
*/
public static function getOwnerDataByNick($nick)
{
@ -122,6 +245,7 @@ class User
* @param string $network network name
*
* @return int group id
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getDefaultGroup($uid, $network = '')
{
@ -148,17 +272,18 @@ class User
/**
* Authenticate a user with a clear text password
*
* @brief Authenticate a user with a clear text password
* @param mixed $user_info
* @brief Authenticate a user with a clear text password
* @param mixed $user_info
* @param string $password
* @param bool $third_party
* @return int|boolean
* @deprecated since version 3.6
* @see User::getIdFromPasswordAuthentication()
* @see User::getIdFromPasswordAuthentication()
*/
public static function authenticate($user_info, $password)
public static function authenticate($user_info, $password, $third_party = false)
{
try {
return self::getIdFromPasswordAuthentication($user_info, $password);
return self::getIdFromPasswordAuthentication($user_info, $password, $third_party);
} catch (Exception $ex) {
return false;
}
@ -168,19 +293,25 @@ class User
* Returns the user id associated with a successful password authentication
*
* @brief Authenticate a user with a clear text password
* @param mixed $user_info
* @param mixed $user_info
* @param string $password
* @param bool $third_party
* @return int User Id if authentication is successful
* @throws Exception
*/
public static function getIdFromPasswordAuthentication($user_info, $password)
public static function getIdFromPasswordAuthentication($user_info, $password, $third_party = false)
{
$user = self::getAuthenticationInfo($user_info);
if (strpos($user['password'], '$') === false) {
if ($third_party && PConfig::get($user['uid'], '2fa', 'verified')) {
// Third-party apps can't verify two-factor authentication, we use app-specific passwords instead
if (AppSpecificPassword::authenticateUser($user['uid'], $password)) {
return $user['uid'];
}
} elseif (strpos($user['password'], '$') === false) {
//Legacy hash that has not been replaced by a new hash yet
if (self::hashPasswordLegacy($password) === $user['password']) {
self::updatePassword($user['uid'], $password);
self::updatePasswordHashed($user['uid'], self::hashPassword($password));
return $user['uid'];
}
@ -188,14 +319,14 @@ class User
//Legacy hash that has been double-hashed and not replaced by a new hash yet
//Warning: `legacy_password` is not necessary in sync with the content of `password`
if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
self::updatePassword($user['uid'], $password);
self::updatePasswordHashed($user['uid'], self::hashPassword($password));
return $user['uid'];
}
} elseif (password_verify($password, $user['password'])) {
//New password hash
if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
self::updatePassword($user['uid'], $password);
self::updatePasswordHashed($user['uid'], self::hashPassword($password));
}
return $user['uid'];
@ -228,7 +359,8 @@ class User
$user = $user_info;
}
if (!isset($user['uid'])
if (
!isset($user['uid'])
|| !isset($user['password'])
|| !isset($user['legacy_password'])
) {
@ -236,7 +368,9 @@ class User
}
} elseif (is_int($user_info) || is_string($user_info)) {
if (is_int($user_info)) {
$user = DBA::selectFirst('user', ['uid', 'password', 'legacy_password'],
$user = DBA::selectFirst(
'user',
['uid', 'password', 'legacy_password'],
[
'uid' => $user_info,
'blocked' => 0,
@ -247,9 +381,11 @@ class User
);
} else {
$fields = ['uid', 'password', 'legacy_password'];
$condition = ["(`email` = ? OR `username` = ? OR `nickname` = ?)
$condition = [
"(`email` = ? OR `username` = ? OR `nickname` = ?)
AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
$user_info, $user_info, $user_info];
$user_info, $user_info, $user_info
];
$user = DBA::selectFirst('user', $fields, $condition);
}
@ -268,7 +404,7 @@ class User
*/
public static function generateNewPassword()
{
return autoname(6) . mt_rand(100, 9999);
return ucfirst(Strings::getRandomName(8)) . random_int(1000, 9999);
}
/**
@ -276,6 +412,7 @@ class User
*
* @param string $password
* @return bool
* @throws Exception
*/
public static function isPasswordExposed($password)
{
@ -284,9 +421,20 @@ class User
'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
]);
$PasswordExposedCHecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
try {
$passwordExposedChecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
return $PasswordExposedCHecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
return $passwordExposedChecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
} catch (\Exception $e) {
Logger::error('Password Exposed Exception: ' . $e->getMessage(), [
'code' => $e->getCode(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
]);
return false;
}
}
/**
@ -305,6 +453,7 @@ class User
*
* @param string $password
* @return string
* @throws Exception
*/
public static function hashPassword($password)
{
@ -321,9 +470,26 @@ class User
* @param int $uid
* @param string $password
* @return bool
* @throws Exception
*/
public static function updatePassword($uid, $password)
{
$password = trim($password);
if (empty($password)) {
throw new Exception(L10n::t('Empty passwords are not allowed.'));
}
if (!Config::get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
throw new Exception(L10n::t('The new password has been exposed in a public data dump, please choose another.'));
}
$allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
throw new Exception(L10n::t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
}
return self::updatePasswordHashed($uid, self::hashPassword($password));
}
@ -334,6 +500,7 @@ class User
* @param int $uid
* @param string $pasword_hashed
* @return bool
* @throws Exception
*/
private static function updatePasswordHashed($uid, $pasword_hashed)
{
@ -355,6 +522,7 @@ class User
*
* @param string $nickname The nickname that should be checked
* @return boolean True is the nickname is blocked on the node
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function isNicknameBlocked($nickname)
{
@ -388,33 +556,35 @@ class User
* - Create self-contact
* - Create profile image
*
* @param array $data
* @return string
* @throw Exception
* @param array $data
* @return array
* @throws \ErrorException
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
* @throws Exception
*/
public static function create(array $data)
{
$a = get_app();
$a = \get_app();
$return = ['user' => null, 'password' => ''];
$using_invites = Config::get('system', 'invitation_only');
$num_invites = Config::get('system', 'number_invites');
$invite_id = !empty($data['invite_id']) ? notags(trim($data['invite_id'])) : '';
$username = !empty($data['username']) ? notags(trim($data['username'])) : '';
$nickname = !empty($data['nickname']) ? notags(trim($data['nickname'])) : '';
$email = !empty($data['email']) ? notags(trim($data['email'])) : '';
$openid_url = !empty($data['openid_url']) ? notags(trim($data['openid_url'])) : '';
$photo = !empty($data['photo']) ? notags(trim($data['photo'])) : '';
$invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : '';
$username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : '';
$nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : '';
$email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : '';
$openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
$photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : '';
$password = !empty($data['password']) ? trim($data['password']) : '';
$password1 = !empty($data['password1']) ? trim($data['password1']) : '';
$confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
$blocked = !empty($data['blocked']) ? intval($data['blocked']) : 0;
$verified = !empty($data['verified']) ? intval($data['verified']) : 0;
$language = !empty($data['language']) ? notags(trim($data['language'])) : 'en';
$blocked = !empty($data['blocked']);
$verified = !empty($data['verified']);
$language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en';
$publish = !empty($data['profile_publish_reg']) && intval($data['profile_publish_reg']) ? 1 : 0;
$netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
$publish = !empty($data['profile_publish_reg']);
$netpublish = $publish && Config::get('system', 'directory');
if ($password1 != $confirm) {
throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
@ -461,8 +631,6 @@ class User
$openid_url = '';
}
$err = '';
// collapse multiple spaces in name
$username = preg_replace('/ +/', ' ', $username);
@ -470,7 +638,7 @@ class User
$username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
if ($username_min_length > $username_max_length) {
logger(L10n::t('system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values.', $username_min_length, $username_max_length), LOGGER_WARNING);
Logger::log(L10n::t('system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values.', $username_min_length, $username_max_length), Logger::WARNING);
$tmp = $username_min_length;
$username_min_length = $username_max_length;
$username_max_length = $tmp;
@ -497,7 +665,7 @@ class User
throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
}
if (!valid_email($email) || !Network::isEmailDomainValid($email)) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
throw new Exception(L10n::t('Not a valid email address.'));
}
if (self::isNicknameBlocked($nickname)) {
@ -524,7 +692,8 @@ class User
}
// Check existing and deleted accounts for this nickname.
if (DBA::exists('user', ['nickname' => $nickname])
if (
DBA::exists('user', ['nickname' => $nickname])
|| DBA::exists('userd', ['username' => $nickname])
) {
throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
@ -669,12 +838,12 @@ class User
}
if (!$photo_failure) {
DBA::update('photo', ['profile' => 1], ['resource-id' => $hash]);
Photo::update(['profile' => 1], ['resource-id' => $hash]);
}
}
}
Addon::callHooks('register_account', $uid);
Hook::callAll('register_account', $uid);
$return['user'] = $user;
return $return;
@ -688,10 +857,12 @@ class User
* @param string $siteurl
* @param string $password Plaintext password
* @return NULL|boolean from notification() and email() inherited
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
{
$body = deindent(L10n::t('
$body = Strings::deindent(L10n::t(
'
Dear %1$s,
Thank you for registering at %2$s. Your account is pending for approval by the administrator.
@ -701,7 +872,11 @@ class User
Login Name: %4$s
Password: %5$s
',
$user['username'], $sitename, $siteurl, $user['nickname'], $password
$user['username'],
$sitename,
$siteurl,
$user['nickname'],
$password
));
return notification([
@ -723,16 +898,20 @@ class User
* @param string $siteurl
* @param string $password Plaintext password
* @return NULL|boolean from notification() and email() inherited
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
{
$preamble = deindent(L10n::t('
Dear %1$s,
$preamble = Strings::deindent(L10n::t(
'
Dear %1$s,
Thank you for registering at %2$s. Your account has been created.
',
$preamble, $user['username'], $sitename
',
$user['username'],
$sitename
));
$body = deindent(L10n::t('
$body = Strings::deindent(L10n::t(
'
The login details are as follows:
Site Location: %3$s
@ -759,7 +938,11 @@ class User
If you ever want to delete your account, you can do so at %3$s/removeme
Thank you and welcome to %2$s.',
$user['email'], $sitename, $siteurl, $user['username'], $password
$user['nickname'],
$sitename,
$siteurl,
$user['username'],
$password
));
return notification([
@ -775,41 +958,166 @@ class User
/**
* @param object $uid user to remove
* @return void
* @return bool
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function remove($uid)
{
if (!$uid) {
return;
return false;
}
$a = get_app();
logger('Removing user: ' . $uid);
Logger::log('Removing user: ' . $uid);
$user = DBA::selectFirst('user', [], ['uid' => $uid]);
Addon::callHooks('remove_user', $user);
Hook::callAll('remove_user', $user);
// save username (actually the nickname as it is guaranteed
// unique), so it cannot be re-registered in the future.
DBA::insert('userd', ['username' => $user['nickname']]);
// The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc(DateTimeFormat::utcNow() . " + 7 day")], ['uid' => $uid]);
Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
// Send an update to the directory
$self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
Worker::add(PRIORITY_LOW, "Directory", $self['url']);
Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
// Remove the user relevant data
Worker::add(PRIORITY_LOW, "RemoveUser", $uid);
Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
if ($uid == local_user()) {
unset($_SESSION['authenticated']);
unset($_SESSION['uid']);
$a->internalRedirect();
return true;
}
/**
* Return all identities to a user
*
* @param int $uid The user id
* @return array All identities for this user
*
* Example for a return:
* [
* [
* 'uid' => 1,
* 'username' => 'maxmuster',
* 'nickname' => 'Max Mustermann'
* ],
* [
* 'uid' => 2,
* 'username' => 'johndoe',
* 'nickname' => 'John Doe'
* ]
* ]
* @throws Exception
*/
public static function identities($uid)
{
$identities = [];
$user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
if (!DBA::isResult($user)) {
return $identities;
}
if ($user['parent-uid'] == 0) {
// First add our own entry
$identities = [[
'uid' => $user['uid'],
'username' => $user['username'],
'nickname' => $user['nickname']
]];
// Then add all the children
$r = DBA::select(
'user',
['uid', 'username', 'nickname'],
['parent-uid' => $user['uid'], 'account_removed' => false]
);
if (DBA::isResult($r)) {
$identities = array_merge($identities, DBA::toArray($r));
}
} else {
// First entry is our parent
$r = DBA::select(
'user',
['uid', 'username', 'nickname'],
['uid' => $user['parent-uid'], 'account_removed' => false]
);
if (DBA::isResult($r)) {
$identities = DBA::toArray($r);
}
// Then add all siblings
$r = DBA::select(
'user',
['uid', 'username', 'nickname'],
['parent-uid' => $user['parent-uid'], 'account_removed' => false]
);
if (DBA::isResult($r)) {
$identities = array_merge($identities, DBA::toArray($r));
}
}
$r = DBA::p(
"SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
FROM `manage`
INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
$user['uid']
);
if (DBA::isResult($r)) {
$identities = array_merge($identities, DBA::toArray($r));
}
return $identities;
}
/**
* Returns statistical information about the current users of this node
*
* @return array
*
* @throws Exception
*/
public static function getStatistics()
{
$statistics = [
'total_users' => 0,
'active_users_halfyear' => 0,
'active_users_monthly' => 0,
];
$userStmt = DBA::p("SELECT `user`.`uid`, `user`.`login_date`, `contact`.`last-item`
FROM `user`
INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid` AND `profile`.`is-default`
INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self`
WHERE (`profile`.`publish` OR `profile`.`net-publish`) AND `user`.`verified`
AND NOT `user`.`blocked` AND NOT `user`.`account_removed`
AND NOT `user`.`account_expired`");
if (!DBA::isResult($userStmt)) {
return $statistics;
}
$halfyear = time() - (180 * 24 * 60 * 60);
$month = time() - (30 * 24 * 60 * 60);
while ($user = DBA::fetch($userStmt)) {
$statistics['total_users']++;
if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
) {
$statistics['active_users_halfyear']++;
}
if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
) {
$statistics['active_users_monthly']++;
}
}
return $statistics;
}
}