use Friendica\Util\XML;
use Friendica\Util\Security;
use Friendica\Util\Strings;
+use Friendica\Util\Network;
use Text_LanguageDetect;
class Item extends BaseObject
// Never reorder or remove entries from this list. Just add new ones at the end, if needed.
// The item-activity table only stores the index and needs this array to know the matching activity.
- const ACTIVITIES = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE];
+ const ACTIVITIES = [ACTIVITY_LIKE, ACTIVITY_DISLIKE, ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE, ACTIVITY_FOLLOW];
private static $legacy_mode = null;
$fields['permissionset'] = ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid'];
- $fields['author'] = ['url' => 'author-link', 'name' => 'author-name',
+ $fields['author'] = ['url' => 'author-link', 'name' => 'author-name', 'addr' => 'author-addr',
'thumb' => 'author-avatar', 'nick' => 'author-nick', 'network' => 'author-network'];
- $fields['owner'] = ['url' => 'owner-link', 'name' => 'owner-name',
+ $fields['owner'] = ['url' => 'owner-link', 'name' => 'owner-name', 'addr' => 'owner-addr',
'thumb' => 'owner-avatar', 'nick' => 'owner-nick', 'network' => 'owner-network'];
$fields['contact'] = ['url' => 'contact-link', 'name' => 'contact-name', 'thumb' => 'contact-avatar',
$item['gravity'] = GRAVITY_PARENT;
} elseif (activity_match($item['verb'], ACTIVITY_POST)) {
$item['gravity'] = GRAVITY_COMMENT;
+ } elseif (activity_match($item['verb'], ACTIVITY_FOLLOW)) {
+ $item['gravity'] = GRAVITY_ACTIVITY;
} else {
$item['gravity'] = GRAVITY_UNKNOWN; // Should not happen
Logger::log('Unknown gravity for verb: ' . $item['verb'], Logger::DEBUG);
$expire_date = time() - ($expire_interval * 86400);
$created_date = strtotime($item['created']);
if ($created_date < $expire_date) {
- Logger::log('item-store: item created ('.date('c', $created_date).') before expiration time ('.date('c', $expire_date).'). ignored. ' . print_r($item,true), Logger::DEBUG);
+ Logger::notice('Item created before expiration interval.', [
+ 'created' => date('c', $created_date),
+ 'expired' => date('c', $expire_date),
+ '$item' => $item
+ ]);
return 0;
}
}
if (DBA::isResult($existing)) {
// We only log the entries with a different user id than 0. Otherwise we would have too many false positives
if ($uid != 0) {
- Logger::log("Item with uri ".$item['uri']." already existed for user ".$uid." with id ".$existing["id"]." target network ".$existing["network"]." - new network: ".$item['network']);
+ Logger::notice('Item already existed for user', [
+ 'uri' => $item['uri'],
+ 'uid' => $uid,
+ 'network' => $item['network'],
+ 'existing_id' => $existing["id"],
+ 'existing_network' => $existing["network"]
+ ]);
}
return $existing["id"];
// When there is no content then we don't post it
if ($item['body'].$item['title'] == '') {
- Logger::log('No body, no title.');
+ Logger::notice('No body, no title.');
return 0;
}
$default = ['url' => $item['author-link'], 'name' => $item['author-name'],
'photo' => $item['author-avatar'], 'network' => $item['network']];
- $item['author-id'] = defaults($item, 'author-id', Contact::getIdForURL($item["author-link"], 0, false, $default));
+ $item['author-id'] = defaults($item, 'author-id', Contact::getIdForURL($item['author-link'], 0, false, $default));
+
+ if (Contact::isBlocked($item['author-id'])) {
+ Logger::notice('Author is blocked node-wide', ['author-link' => $item['author-link'], 'item-uri' => $item['uri']]);
+ return 0;
+ }
- if (Contact::isBlocked($item["author-id"])) {
- Logger::log('Contact '.$item["author-id"].' is blocked, item '.$item["uri"].' will not be stored');
+ if (!empty($item['author-link']) && Network::isUrlBlocked($item['author-link'])) {
+ Logger::notice('Author server is blocked', ['author-link' => $item['author-link'], 'item-uri' => $item['uri']]);
return 0;
}
$default = ['url' => $item['owner-link'], 'name' => $item['owner-name'],
'photo' => $item['owner-avatar'], 'network' => $item['network']];
- $item['owner-id'] = defaults($item, 'owner-id', Contact::getIdForURL($item["owner-link"], 0, false, $default));
+ $item['owner-id'] = defaults($item, 'owner-id', Contact::getIdForURL($item['owner-link'], 0, false, $default));
- if (Contact::isBlocked($item["owner-id"])) {
- Logger::log('Contact '.$item["owner-id"].' is blocked, item '.$item["uri"].' will not be stored');
+ if (Contact::isBlocked($item['owner-id'])) {
+ Logger::notice('Owner is blocked node-wide', ['owner-link' => $item['owner-link'], 'item-uri' => $item['uri']]);
return 0;
}
- if ($item['network'] == Protocol::PHANTOM) {
- Logger::log('Missing network. Called by: '.System::callstack(), Logger::DEBUG);
+ if (!empty($item['owner-link']) && Network::isUrlBlocked($item['owner-link'])) {
+ Logger::notice('Owner server is blocked', ['owner-link' => $item['owner-link'], 'item-uri' => $item['uri']]);
+ return 0;
+ }
+ if ($item['network'] == Protocol::PHANTOM) {
$item['network'] = Protocol::DFRN;
- Logger::log("Set network to " . $item["network"] . " for " . $item["uri"], Logger::DEBUG);
+ Logger::notice('Missing network, setting to {network}.', [
+ 'uri' => $item["uri"],
+ 'network' => $item['network'],
+ 'callstack' => System::callstack()
+ ]);
}
// Checking if there is already an item with the same guid
- Logger::log('Checking for an item for user '.$item['uid'].' on network '.$item['network'].' with the guid '.$item['guid'], Logger::DEBUG);
$condition = ['guid' => $item['guid'], 'network' => $item['network'], 'uid' => $item['uid']];
if (self::exists($condition)) {
- Logger::log('found item with guid '.$item['guid'].' for user '.$item['uid'].' on network '.$item['network'], Logger::DEBUG);
+ Logger::notice('Found already existing item', [
+ 'guid' => $item['guid'],
+ 'uid' => $item['uid'],
+ 'network' => $item['network']
+ ]);
return 0;
}
+ if ($item['verb'] == ACTIVITY_FOLLOW) {
+ if (!$item['origin'] && ($item['author-id'] == Contact::getPublicIdByUserId($uid))) {
+ // Our own follow request can be relayed to us. We don't store it to avoid notification chaos.
+ Logger::log("Follow: Don't store not origin follow request from us for " . $item['parent-uri'], Logger::DEBUG);
+ return 0;
+ }
+
+ $condition = ['verb' => ACTIVITY_FOLLOW, 'uid' => $item['uid'],
+ 'parent-uri' => $item['parent-uri'], 'author-id' => $item['author-id']];
+ if (self::exists($condition)) {
+ // It happens that we receive multiple follow requests by the same author - we only store one.
+ Logger::log('Follow: Found existing follow request from author ' . $item['author-id'] . ' for ' . $item['parent-uri'], Logger::DEBUG);
+ return 0;
+ }
+ }
+
// Check for hashtags in the body and repair or add hashtag links
self::setHashtags($item);
$item['private'] = 0;
}
- // If its a post from myself then tag the thread as "mention"
- Logger::log("Checking if parent ".$parent_id." has to be tagged as mention for user ".$item['uid'], Logger::DEBUG);
- $user = DBA::selectFirst('user', ['nickname'], ['uid' => $item['uid']]);
- if (DBA::isResult($user)) {
- $self = Strings::normaliseLink(System::baseUrl() . '/profile/' . $user['nickname']);
- $self_id = Contact::getIdForURL($self, 0, true);
- Logger::log("'myself' is ".$self_id." for parent ".$parent_id." checking against ".$item['author-id']." and ".$item['owner-id'], Logger::DEBUG);
- if (($item['author-id'] == $self_id) || ($item['owner-id'] == $self_id)) {
- DBA::update('thread', ['mention' => true], ['iid' => $parent_id]);
- Logger::log("tagged thread ".$parent_id." as mention for user ".$self, Logger::DEBUG);
- }
+ // If its a post that originated here then tag the thread as "mention"
+ if ($item['origin'] && $item['uid']) {
+ DBA::update('thread', ['mention' => true], ['iid' => $parent_id]);
+ Logger::log('tagged thread ' . $parent_id . ' as mention for user ' . $item['uid'], Logger::DEBUG);
}
} else {
/*
$item["global"] = true;
// Set the global flag on all items if this was a global item entry
- self::update(['global' => true], ['uri' => $item["uri"]]);
+ DBA::update('item', ['global' => true], ['uri' => $item["uri"]]);
} else {
$item["global"] = self::exists(['uid' => 0, 'uri' => $item["uri"]]);
}
}
// Set parent id
- self::update(['parent' => $parent_id], ['id' => $current_post]);
+ DBA::update('item', ['parent' => $parent_id], ['id' => $current_post]);
$item['id'] = $current_post;
$item['parent'] = $parent_id;
// update the commented timestamp on the parent
// Only update "commented" if it is really a comment
if (($item['gravity'] != GRAVITY_ACTIVITY) || !Config::get("system", "like_no_comment")) {
- self::update(['commented' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
+ DBA::update('item', ['commented' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
} else {
- self::update(['changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
+ DBA::update('item', ['changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]);
}
if ($dsprsig) {
DBA::insert('diaspora-interaction', ['uri-id' => $item['uri-id'], 'interaction' => $diaspora_signed_text], true);
}
- $deleted = self::tagDeliver($item['uid'], $current_post);
+ self::tagDeliver($item['uid'], $current_post);
/*
* current post can be deleted if is for a community page and no mention are
* in it.
*/
- if (!$deleted && !$dontcache) {
+ if (!$dontcache) {
$posted_item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $current_post]);
if (DBA::isResult($posted_item)) {
if ($notify) {
$condition = ['id' => $itemid, 'uid' => 0,
'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""],
'visible' => true, 'deleted' => false, 'moderated' => false, 'private' => false];
- $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]);
+ $item = self::selectFirst(self::ITEM_FIELDLIST, $condition);
if (!DBA::isResult($item)) {
return;
}
public static function setHashtags(&$item)
{
-
$tags = BBCode::getTags($item["body"]);
// No hashtags?
return false;
}
+ // What happens in [code], stays in [code]!
+ // escape the # and the [
+ // hint: we will also get in trouble with #tags, when we want markdown in posts -> ### Headline 3
+ $item["body"] = preg_replace_callback("/\[code(.*?)\](.*?)\[\/code\]/ism",
+ function ($match) {
+ // we truly ESCape all # and [ to prevent gettin weird tags in [code] blocks
+ $find = ['#', '['];
+ $replace = [chr(27).'sharp', chr(27).'leftsquarebracket'];
+ return ("[code" . $match[1] . "]" . str_replace($find, $replace, $match[2]) . "[/code]");
+ }, $item["body"]);
+
// This sorting is important when there are hashtags that are part of other hashtags
// Otherwise there could be problems with hashtags like #test and #test2
rsort($tags);
"#$2", $item["body"]);
foreach ($tags as $tag) {
- if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=')) {
+ if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=') || $tag[1] == '#') {
continue;
}
$basetag = str_replace('_',' ',substr($tag,1));
-
$newtag = '#[url=' . System::baseUrl() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
$item["body"] = str_replace($tag, $newtag, $item["body"]);
// Convert back the masked hashtags
$item["body"] = str_replace("#", "#", $item["body"]);
+
+ // Remember! What happens in [code], stays in [code]
+ // roleback the # and [
+ $item["body"] = preg_replace_callback("/\[code(.*?)\](.*?)\[\/code\]/ism",
+ function ($match) {
+ // we truly unESCape all sharp and leftsquarebracket
+ $find = [chr(27).'sharp', chr(27).'leftsquarebracket'];
+ $replace = ['#', '['];
+ return ("[code" . $match[1] . "]" . str_replace($find, $replace, $match[2]) . "[/code]");
+ }, $item["body"]);
}
public static function getGuidById($id)
$id = 0;
if ($uid == 0) {
- $uid == local_user();
+ $uid = local_user();
}
// Does the given user have this item?
return $ret;
}
+
+ /**
+ * Is the given item array a post that is sent as starting post to a forum?
+ *
+ * @param array $item
+ * @param array $owner
+ *
+ * @return boolean "true" when it is a forum post
+ */
+ public static function isForumPost(array $item, array $owner = [])
+ {
+ if (empty($owner)) {
+ $owner = User::getOwnerDataById($item['uid']);
+ if (empty($owner)) {
+ return false;
+ }
+ }
+
+ if (($item['author-id'] == $item['owner-id']) ||
+ ($owner['id'] == $item['contact-id']) ||
+ ($item['uri'] != $item['parent-uri']) ||
+ $item['origin']) {
+ return false;
+ }
+
+ return Contact::isForum($item['contact-id']);
+ }
}