X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FItem.php;h=3f80b11c31009978ffb7f54d72dc0f11011bed57;hb=bffdf96d8791db3f5c1aaede9b3f7416fb189f38;hp=d4c5e81704aaf08f5b0e9fae328f5b77380e4e6c;hpb=22816c49a1972fff2e201dd66ac59b80624ee8aa;p=friendica.git diff --git a/src/Model/Item.php b/src/Model/Item.php index d4c5e81704..3f80b11c31 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -24,6 +24,7 @@ use Friendica\Protocol\Diaspora; use Friendica\Protocol\OStatus; use Friendica\Util\DateTimeFormat; use Friendica\Util\XML; +use Friendica\Util\Lock; use dba; use Text_LanguageDetect; @@ -34,15 +35,15 @@ require_once 'include/text.php'; class Item extends BaseObject { // Field list that is used to display the items - const DISPLAY_FIELDLIST = ['uid', 'id', 'parent', 'uri', 'thr-parent', 'parent-uri', 'guid', + const DISPLAY_FIELDLIST = ['uid', 'id', 'parent', 'uri', 'thr-parent', 'parent-uri', 'guid', 'network', 'commented', 'created', 'edited', 'received', 'verb', 'object-type', 'postopts', 'plink', - 'wall', 'private', 'starred', 'origin', 'title', 'body', 'file', 'attach', + 'wall', 'private', 'starred', 'origin', 'title', 'body', 'file', 'attach', 'language', 'content-warning', 'location', 'coord', 'app', 'rendered-hash', 'rendered-html', 'object', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'item_id', - 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network', + 'author-id', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'contact-id', 'contact-link', 'contact-name', 'contact-avatar', - 'network', 'url', 'name', 'writable', 'self', 'cid', 'alias', + 'writable', 'self', 'cid', 'alias', 'event-id', 'event-created', 'event-edited', 'event-start', 'event-finish', 'event-summary', 'event-desc', 'event-location', 'event-type', 'event-nofinish', 'event-adjust', 'event-ignore', 'event-id']; @@ -56,6 +57,27 @@ class Item extends BaseObject 'author-id', 'author-link', 'owner-link', 'contact-uid', 'signed_text', 'signature', 'signer']; + // Field list for "item-content" table that is mixed with the item table + const MIXED_CONTENT_FIELDLIST = ['title', 'content-warning', 'body', 'location', + 'coord', 'app', 'rendered-hash', 'rendered-html', 'verb', + 'object-type', 'object', 'target-type', 'target', 'plink']; + + // Field list for "item-content" table that is not present in the "item" table + const CONTENT_FIELDLIST = ['language']; + + // All fields in the item table + const ITEM_FIELDLIST = ['id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', 'guid', + 'contact-id', 'type', 'wall', 'gravity', 'extid', 'icid', + 'created', 'edited', 'commented', 'received', 'changed', 'verb', + 'postopts', 'plink', 'resource-id', 'event-id', 'tag', 'attach', 'inform', + 'file', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', + 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark', + 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'network', + 'title', 'content-warning', 'body', 'location', 'coord', 'app', + 'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target', + 'author-id', 'author-link', 'author-name', 'author-avatar', + 'owner-id', 'owner-link', 'owner-name', 'owner-avatar']; + /** * @brief Fetch a single item row * @@ -66,6 +88,51 @@ class Item extends BaseObject { $row = dba::fetch($stmt); + // Fetch data from the item-content table whenever there is content there + foreach (self::MIXED_CONTENT_FIELDLIST as $field) { + if (empty($row[$field]) && !empty($row['item-' . $field])) { + $row[$field] = $row['item-' . $field]; + } + unset($row['item-' . $field]); + } + + // We prefer the data from the user's contact over the public one + if (!empty($row['author-link']) && !empty($row['contact-link']) && + ($row['author-link'] == $row['contact-link'])) { + if (isset($row['author-avatar']) && !empty($row['contact-avatar'])) { + $row['author-avatar'] = $row['contact-avatar']; + } + if (isset($row['author-name']) && !empty($row['contact-name'])) { + $row['author-name'] = $row['contact-name']; + } + } + + if (!empty($row['owner-link']) && !empty($row['contact-link']) && + ($row['owner-link'] == $row['contact-link'])) { + if (isset($row['owner-avatar']) && !empty($row['contact-avatar'])) { + $row['owner-avatar'] = $row['contact-avatar']; + } + if (isset($row['owner-name']) && !empty($row['contact-name'])) { + $row['owner-name'] = $row['contact-name']; + } + } + + // Build the tag string out of the term entries + if (isset($row['id']) && array_key_exists('tag', $row)) { + $row['tag'] = Term::tagTextFromItemId($row['id']); + } + + // Build the file string out of the term entries + if (isset($row['id']) && array_key_exists('file', $row)) { + $row['file'] = Term::fileTextFromItemId($row['id']); + } + + // We can always comment on posts from these networks + if (isset($row['writable']) && !empty($row['network']) && + in_array($row['network'], [NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS])) { + $row['writable'] = true; + } + return $row; } @@ -90,6 +157,27 @@ class Item extends BaseObject return $data; } + /** + * @brief Check if item data exists + * + * @param array $condition array of fields for condition + * + * @return boolean Are there rows for that condition? + */ + public static function exists($condition) { + $stmt = self::select(['id'], $condition, ['limit' => 1]); + + if (is_bool($stmt)) { + $retval = $stmt; + } else { + $retval = (dba::num_rows($stmt) > 0); + } + + dba::close($stmt); + + return $retval; + } + /** * Retrieve a single record from the item table for a given user and returns it in an associative array * @@ -324,24 +412,24 @@ class Item extends BaseObject $fields['item'] = ['id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', 'guid', 'contact-id', 'owner-id', 'author-id', 'type', 'wall', 'gravity', 'extid', - 'created', 'edited', 'commented', 'received', 'changed', - 'title', 'body', 'app', 'verb', 'object-type', 'object', 'target-type', 'target', - 'postopts', 'plink', 'resource-id', 'event-id', 'tag', 'attach', 'inform', - 'file', 'location', 'coord', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', + 'created', 'edited', 'commented', 'received', 'changed', 'postopts', + 'resource-id', 'event-id', 'tag', 'attach', 'inform', + 'file', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark', - 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', - 'rendered-hash', 'rendered-html', 'global', 'content-warning', - 'id' => 'item_id', 'network']; + 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', + 'id' => 'item_id', 'network', 'icid']; + + $fields['item-content'] = array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST); $fields['author'] = ['url' => 'author-link', 'name' => 'author-name', - 'thumb' => 'author-avatar', 'nick' => 'author-nick', 'network' => 'author-network']; + 'thumb' => 'author-avatar', 'nick' => 'author-nick']; $fields['owner'] = ['url' => 'owner-link', 'name' => 'owner-name', - 'thumb' => 'owner-avatar', 'nick' => 'owner-nick', 'network' => 'owner-network']; + 'thumb' => 'owner-avatar', 'nick' => 'owner-nick']; $fields['contact'] = ['url' => 'contact-link', 'name' => 'contact-name', 'thumb' => 'contact-avatar', - 'url', 'name', 'writable', 'self', 'id' => 'cid', 'alias', 'uid' => 'contact-uid', - 'photo', 'name-date', 'uri-date', 'avatar-date', 'thumb', 'dfrn-id', 'network' => 'contact-network']; + 'writable', 'self', 'id' => 'cid', 'alias', 'uid' => 'contact-uid', + 'photo', 'name-date', 'uri-date', 'avatar-date', 'thumb', 'dfrn-id']; $fields['parent-item'] = ['guid' => 'parent-guid', 'network' => 'parent-network']; @@ -422,6 +510,10 @@ class Item extends BaseObject $joins .= " LEFT JOIN `sign` ON `sign`.`iid` = `item`.`id`"; } + if (strpos($sql_commands, "`item-content`.") !== false) { + $joins .= " LEFT JOIN `item-content` ON `item-content`.`id` = `item`.`icid`"; + } + if ((strpos($sql_commands, "`parent-item`.") !== false) || (strpos($sql_commands, "`parent-author`.") !== false)) { $joins .= " STRAIGHT_JOIN `item` AS `parent-item` ON `parent-item`.`id` = `item`.`parent`"; } @@ -443,14 +535,27 @@ class Item extends BaseObject */ private static function constructSelectFields($fields, $selected) { + // To be able to fetch the tags we need the item id + if (in_array('tag', $selected) && !in_array('id', $selected)) { + $selected[] = 'id'; + } + + // To be able to fetch the files we need the item id + if (in_array('file', $selected) && !in_array('id', $selected)) { + $selected[] = 'id'; + } + $selection = []; foreach ($fields as $table => $table_fields) { foreach ($table_fields as $field => $select) { if (empty($selected) || in_array($select, $selected)) { + if (in_array($select, self::MIXED_CONTENT_FIELDLIST)) { + $selection[] = "`item`.`".$select."` AS `item-" . $select . "`"; + } if (is_int($field)) { - $selection[] = "`" . $table . "`.`".$select."`"; + $selection[] = "`" . $table . "`.`" . $select . "`"; } else { - $selection[] = "`" . $table . "`.`" . $field . "` AS `".$select ."`"; + $selection[] = "`" . $table . "`.`" . $field . "` AS `" . $select . "`"; } } } @@ -509,21 +614,57 @@ class Item extends BaseObject // We cannot simply expand the condition to check for origin entries // The condition needn't to be a simple array but could be a complex condition. // And we have to execute this query before the update to ensure to fetch the same data. - $items = dba::select('item', ['id', 'origin'], $condition); + $items = dba::select('item', ['id', 'origin', 'uri', 'plink'], $condition); + + $content_fields = []; + foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST) as $field) { + if (isset($fields[$field])) { + $content_fields[$field] = $fields[$field]; + unset($fields[$field]); + } + } - $success = dba::update('item', $fields, $condition); + if (array_key_exists('tag', $fields)) { + $tags = $fields['tag']; + unset($fields['tag']); + } else { + $tags = ''; + } - if (!$success) { - dba::close($items); - dba::rollback(); - return false; + if (array_key_exists('file', $fields)) { + $files = $fields['file']; + unset($fields['file']); + } else { + $files = ''; + } + + if (!empty($fields)) { + $success = dba::update('item', $fields, $condition); + + if (!$success) { + dba::close($items); + dba::rollback(); + return false; + } } + // When there is no content for the "old" item table, this will count the fetched items $rows = dba::affected_rows(); while ($item = dba::fetch($items)) { - Term::insertFromTagFieldByItemId($item['id']); - Term::insertFromFileFieldByItemId($item['id']); + if (!empty($item['plink'])) { + $content_fields['plink'] = $item['plink']; + } + self::updateContent($content_fields, ['uri' => $item['uri']]); + + if (!empty($tags)) { + Term::insertFromTagFieldByItemId($item['id'], $tags); + } + + if (!empty($files)) { + Term::insertFromFileFieldByItemId($item['id'], $files); + } + self::updateThread($item['id']); // We only need to notfiy others when it is an original entry from us. @@ -593,7 +734,7 @@ class Item extends BaseObject $fields = ['id', 'uri', 'uid', 'parent', 'parent-uri', 'origin', 'deleted', 'file', 'resource-id', 'event-id', 'attach', 'verb', 'object-type', 'object', 'target', 'contact-id']; - $item = dba::selectFirst('item', $fields, ['id' => $item_id]); + $item = self::selectFirst($fields, ['id' => $item_id]); if (!DBM::is_result($item)) { logger('Item with ID ' . $item_id . " hasn't been found.", LOGGER_DEBUG); return false; @@ -604,7 +745,7 @@ class Item extends BaseObject return false; } - $parent = dba::selectFirst('item', ['origin'], ['id' => $item['parent']]); + $parent = self::selectFirst(['origin'], ['id' => $item['parent']]); if (!DBM::is_result($parent)) { $parent = ['origin' => false]; } @@ -653,15 +794,18 @@ class Item extends BaseObject self::deleteTagsFromItem($item); // Set the item to "deleted" - dba::update('item', ['deleted' => true, 'title' => '', 'body' => '', - 'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()], - ['id' => $item['id']]); - - Term::insertFromTagFieldByItemId($item['id']); - Term::insertFromFileFieldByItemId($item['id']); + // This erasing of item content is superfluous for items with a matching item-content. + // But for the next time we will still have old content in the item table. + $item_fields = ['deleted' => true, 'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow(), + 'body' => '', 'title' => '', 'content-warning' => '', 'rendered-hash' => '', 'rendered-html' => '', + 'object' => '', 'target' => '', 'tag' => '', 'postopts' => '', 'attach' => '', 'file' => '']; + dba::update('item', $item_fields, ['id' => $item['id']]); + + Term::insertFromTagFieldByItemId($item['id'], ''); + Term::insertFromFileFieldByItemId($item['id'], ''); self::deleteThread($item['id'], $item['parent-uri']); - if (!dba::exists('item', ["`uri` = ? AND `uid` != 0 AND NOT `deleted`", $item['uri']])) { + if (!self::exists(["`uri` = ? AND `uid` != 0 AND NOT `deleted`", $item['uri']])) { self::delete(['uri' => $item['uri'], 'uid' => 0, 'deleted' => false], $priority); } @@ -681,7 +825,7 @@ class Item extends BaseObject } elseif ($item['uid'] != 0) { // When we delete just our local user copy of an item, we have to set a marker to hide it - $global_item = dba::selectFirst('item', ['id'], ['uri' => $item['uri'], 'uid' => 0, 'deleted' => false]); + $global_item = self::selectFirst(['id'], ['uri' => $item['uri'], 'uid' => 0, 'deleted' => false]); if (DBM::is_result($global_item)) { dba::update('user-item', ['hidden' => true], ['iid' => $global_item['id'], 'uid' => $item['uid']], true); } @@ -705,7 +849,7 @@ class Item extends BaseObject return; } - $i = dba::selectFirst('item', ['id', 'contact-id', 'tag'], ['uri' => $xt->id, 'uid' => $item['uid']]); + $i = self::selectFirst(['id', 'contact-id', 'tag'], ['uri' => $xt->id, 'uid' => $item['uid']]); if (!DBM::is_result($i)) { return; } @@ -878,18 +1022,21 @@ class Item extends BaseObject $item['parent-uri'] = $item['thr-parent']; } - if (x($item, 'gravity')) { + $item['type'] = defaults($item, 'type', 'remote'); + + if (isset($item['gravity'])) { $item['gravity'] = intval($item['gravity']); } elseif ($item['parent-uri'] === $item['uri']) { - $item['gravity'] = 0; - } elseif (activity_match($item['verb'],ACTIVITY_POST)) { - $item['gravity'] = 6; + $item['gravity'] = GRAVITY_PARENT; + } elseif (activity_match($item['verb'], ACTIVITY_POST)) { + $item['gravity'] = GRAVITY_COMMENT; + } elseif ($item['type'] == 'activity') { + $item['gravity'] = GRAVITY_ACTIVITY; } else { - $item['gravity'] = 6; // extensible catchall + $item['gravity'] = GRAVITY_UNKNOWN; // Should not happen + logger('Unknown gravity for verb: ' . $item['verb'] . ' - type: ' . $item['type'], LOGGER_DEBUG); } - $item['type'] = defaults($item, 'type', 'remote'); - $uid = intval($item['uid']); // check for create date and expire time @@ -918,7 +1065,7 @@ class Item extends BaseObject $condition = ["`uri` = ? AND `uid` = ? AND `network` IN (?, ?, ?)", trim($item['uri']), $item['uid'], NETWORK_DIASPORA, NETWORK_DFRN, NETWORK_OSTATUS]; - $existing = dba::selectFirst('item', ['id', 'network'], $condition); + $existing = self::selectFirst(['id', 'network'], $condition); if (DBM::is_result($existing)) { // We only log the entries with a different user id than 0. Otherwise we would have too many false positives if ($uid != 0) { @@ -929,7 +1076,7 @@ class Item extends BaseObject } } - self::addLanguageInPostopts($item); + self::addLanguageToItemArray($item); $item['wall'] = intval(defaults($item, 'wall', 0)); $item['extid'] = trim(defaults($item, 'extid', '')); @@ -975,6 +1122,7 @@ class Item extends BaseObject // When there is no content then we don't post it if ($item['body'].$item['title'] == '') { + logger('No body, no title.'); return 0; } @@ -1017,11 +1165,12 @@ class Item extends BaseObject return 0; } - //unset($item['author-link']); + // These fields aren't stored anymore in the item table, they are fetched upon request + unset($item['author-link']); unset($item['author-name']); unset($item['author-avatar']); - //unset($item['owner-link']); + unset($item['owner-link']); unset($item['owner-name']); unset($item['owner-avatar']); @@ -1040,7 +1189,7 @@ class Item extends BaseObject // Checking if there is already an item with the same guid logger('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 (dba::exists('item', $condition)) { + if (self::exists($condition)) { logger('found item with guid '.$item['guid'].' for user '.$item['uid'].' on network '.$item['network'], LOGGER_DEBUG); return 0; } @@ -1073,7 +1222,7 @@ class Item extends BaseObject 'wall', 'private', 'forum_mode', 'origin']; $condition = ['uri' => $item['parent-uri'], 'uid' => $item['uid']]; $params = ['order' => ['id' => false]]; - $parent = dba::selectFirst('item', $fields, $condition, $params); + $parent = self::selectFirst($fields, $condition, $params); if (DBM::is_result($parent)) { // is the new message multi-level threaded? @@ -1087,7 +1236,7 @@ class Item extends BaseObject 'parent-uri' => $item['parent-uri'], 'uid' => $item['uid']]; $params = ['order' => ['id' => false]]; - $toplevel_parent = dba::selectFirst('item', $fields, $condition, $params); + $toplevel_parent = self::selectFirst($fields, $condition, $params); if (DBM::is_result($toplevel_parent)) { $parent = $toplevel_parent; @@ -1126,8 +1275,9 @@ class Item extends BaseObject $user = dba::selectFirst('user', ['nickname'], ['uid' => $item['uid']]); if (DBM::is_result($user)) { $self = normalise_link(System::baseUrl() . '/profile/' . $user['nickname']); - logger("'myself' is ".$self." for parent ".$parent_id." checking against ".$item['author-link']." and ".$item['owner-link'], LOGGER_DEBUG); - if ((normalise_link($item['author-link']) == $self) || (normalise_link($item['owner-link']) == $self)) { + $self_id = Contact::getIdForURL($self, 0, true); + logger("'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("tagged thread ".$parent_id." as mention for user ".$self, LOGGER_DEBUG); } @@ -1141,7 +1291,7 @@ class Item extends BaseObject logger('$force_parent=true, reply converted to top-level post.'); $parent_id = 0; $item['parent-uri'] = $item['uri']; - $item['gravity'] = 0; + $item['gravity'] = GRAVITY_PARENT; } else { logger('item parent '.$item['parent-uri'].' for '.$item['uid'].' was not found - ignoring item'); return 0; @@ -1153,7 +1303,7 @@ class Item extends BaseObject $condition = ["`uri` = ? AND `network` IN (?, ?) AND `uid` = ?", $item['uri'], $item['network'], NETWORK_DFRN, $item['uid']]; - if (dba::exists('item', $condition)) { + if (self::exists($condition)) { logger('duplicated item with the same uri found. '.print_r($item,true)); return 0; } @@ -1161,7 +1311,7 @@ class Item extends BaseObject // On Friendica and Diaspora the GUID is unique if (in_array($item['network'], [NETWORK_DFRN, NETWORK_DIASPORA])) { $condition = ['guid' => $item['guid'], 'uid' => $item['uid']]; - if (dba::exists('item', $condition)) { + if (self::exists($condition)) { logger('duplicated item with the same guid found. '.print_r($item,true)); return 0; } @@ -1169,7 +1319,7 @@ class Item extends BaseObject // Check for an existing post with the same content. There seems to be a problem with OStatus. $condition = ["`body` = ? AND `network` = ? AND `created` = ? AND `contact-id` = ? AND `uid` = ?", $item['body'], $item['network'], $item['created'], $item['contact-id'], $item['uid']]; - if (dba::exists('item', $condition)) { + if (self::exists($condition)) { logger('duplicated item with the same body found. '.print_r($item,true)); return 0; } @@ -1182,7 +1332,7 @@ class Item extends BaseObject // Set the global flag on all items if this was a global item entry dba::update('item', ['global' => true], ['uri' => $item["uri"]]); } else { - $item["global"] = dba::exists('item', ['uid' => 0, 'uri' => $item["uri"]]); + $item["global"] = self::exists(['uid' => 0, 'uri' => $item["uri"]]); } // ACL settings @@ -1223,7 +1373,7 @@ class Item extends BaseObject * An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this. */ if ($item["uid"] == 0) { - if (dba::exists('item', ['uri' => trim($item['uri']), 'uid' => 0])) { + if (self::exists(['uri' => trim($item['uri']), 'uid' => 0])) { logger('Global item already stored. URI: '.$item['uri'].' on network '.$item['network'], LOGGER_DEBUG); return 0; } @@ -1231,6 +1381,23 @@ class Item extends BaseObject logger('' . print_r($item,true), LOGGER_DATA); + if (array_key_exists('tag', $item)) { + $tags = $item['tag']; + unset($item['tag']); + } else { + $tags = ''; + } + + if (array_key_exists('file', $item)) { + $files = $item['file']; + unset($item['file']); + } else { + $files = ''; + } + + // We are doing this outside of the transaction to avoid timing problems + self::insertContent($item); + dba::transaction(); $ret = dba::insert('item', $item); @@ -1302,7 +1469,7 @@ class Item extends BaseObject // update the commented timestamp on the parent // Only update "commented" if it is really a comment - if (($item['verb'] == ACTIVITY_POST) || !Config::get("system", "like_no_comment")) { + if (($item['gravity'] != GRAVITY_ACTIVITY) || !Config::get("system", "like_no_comment")) { dba::update('item', ['commented' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]); } else { dba::update('item', ['changed' => DateTimeFormat::utcNow()], ['id' => $parent_id]); @@ -1335,7 +1502,7 @@ class Item extends BaseObject * in it. */ if (!$deleted && !$dontcache) { - $posted_item = dba::selectFirst('item', [], ['id' => $current_post]); + $posted_item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $current_post]); if (DBM::is_result($posted_item)) { if ($notify) { Addon::callHooks('post_local_end', $posted_item); @@ -1359,8 +1526,13 @@ class Item extends BaseObject * Due to deadlock issues with the "term" table we are doing these steps after the commit. * This is not perfect - but a workable solution until we found the reason for the problem. */ - Term::insertFromTagFieldByItemId($current_post); - Term::insertFromFileFieldByItemId($current_post); + if (!empty($tags)) { + Term::insertFromTagFieldByItemId($current_post, $tags); + } + + if (!empty($files)) { + Term::insertFromFileFieldByItemId($current_post, $files); + } if ($item['parent-uri'] === $item['uri']) { self::addShadow($current_post); @@ -1379,6 +1551,96 @@ class Item extends BaseObject return $current_post; } + /** + * @brief Insert a new item content entry + * + * @param array $item The item fields that are to be inserted + */ + private static function insertContent(&$item) + { + $fields = ['uri' => $item['uri'], 'plink' => $item['plink'], + 'uri-plink-hash' => hash('sha1', $item['plink']).hash('sha1', $item['uri'])]; + + foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST) as $field) { + if (isset($item[$field])) { + $fields[$field] = $item[$field]; + unset($item[$field]); + } + } + + // To avoid timing problems, we are using locks. + $locked = Lock::set('item_insert_content'); + if (!$locked) { + logger("Couldn't acquire lock for URI " . $item['uri'] . " - proceeding anyway."); + } + + // Do we already have this content? + $item_content = dba::selectFirst('item-content', ['id'], ['uri' => $item['uri']]); + if (DBM::is_result($item_content)) { + $item['icid'] = $item_content['id']; + logger('Fetched content for URI ' . $item['uri'] . ' (' . $item['icid'] . ')'); + } elseif (dba::insert('item-content', $fields)) { + $item['icid'] = dba::lastInsertId(); + logger('Inserted content for URI ' . $item['uri'] . ' (' . $item['icid'] . ')'); + } else { + // By setting the ICID value through the worker we should avoid timing problems. + // When the locking works, this shouldn't be needed. But better be prepared. + Worker::add(PRIORITY_HIGH, 'SetItemContentID', $item['uri']); + logger('Could not insert content for URI ' . $item['uri'] . ' - trying asynchronously'); + } + if ($locked) { + Lock::remove('item_insert_content'); + } + } + + /** + * @brief Set the item content id for a given URI + * + * @param string $uri The item URI + */ + public static function setICIDforURI($uri) + { + $item_content = dba::selectFirst('item-content', ['id'], ['uri' => $uri]); + if (DBM::is_result($item_content)) { + dba::update('item', ['icid' => $item_content['id']], ['icid' => 0, 'uri' => $uri]); + logger('Asynchronously set item content id for URI ' . $uri . ' (' . $item_content['id'] . ') - Affected: '. (int)dba::affected_rows()); + } else { + logger('No item-content found for URI ' . $uri); + } + } + + /** + * @brief Update existing item content entries + * + * @param array $item The item fields that are to be changed + * @param array $condition The condition for finding the item content entries + */ + private static function updateContent($item, $condition) + { + // We have to select only the fields from the "item-content" table + $fields = []; + foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST) as $field) { + if (isset($item[$field])) { + $fields[$field] = $item[$field]; + } + } + + if (empty($fields)) { + return; + } + + if (!empty($item['plink'])) { + $fields['uri-plink-hash'] = hash('sha1', $item['plink']) . hash('sha1', $condition['uri']); + } else { + // Ensure that we don't delete the plink + unset($fields['plink']); + } + + logger('Update content for URI ' . $condition['uri']); + + dba::update('item-content', $fields, $condition, true); + } + /** * @brief Distributes public items to the receivers * @@ -1388,7 +1650,7 @@ class Item extends BaseObject public static function distribute($itemid, $signed_text = '') { $condition = ["`id` IN (SELECT `parent` FROM `item` WHERE `id` = ?)", $itemid]; - $parent = dba::selectFirst('item', ['owner-id'], $condition); + $parent = self::selectFirst(['owner-id'], $condition); if (!DBM::is_result($parent)) { return; } @@ -1397,7 +1659,7 @@ class Item extends BaseObject $condition = ['id' => $itemid, 'uid' => 0, 'network' => [NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""], 'visible' => true, 'deleted' => false, 'moderated' => false, 'private' => false]; - $item = dba::selectFirst('item', [], ['id' => $itemid]); + $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]); if (!DBM::is_result($item)) { return; } @@ -1408,8 +1670,6 @@ class Item extends BaseObject unset($item['wall']); unset($item['origin']); unset($item['starred']); - unset($item['rendered-hash']); - unset($item['rendered-html']); $users = []; @@ -1423,7 +1683,7 @@ class Item extends BaseObject $origin_uid = 0; if ($item['uri'] != $item['parent-uri']) { - $parents = dba::select('item', ['uid', 'origin'], ["`uri` = ? AND `uid` != 0", $item['parent-uri']]); + $parents = self::select(['uid', 'origin'], ["`uri` = ? AND `uid` != 0", $item['parent-uri']]); while ($parent = dba::fetch($parents)) { $users[$parent['uid']] = $parent['uid']; if ($parent['origin'] && !$item['origin']) { @@ -1502,9 +1762,9 @@ class Item extends BaseObject */ public static function addShadow($itemid) { - $fields = ['uid', 'private', 'moderated', 'visible', 'deleted', 'network']; + $fields = ['uid', 'private', 'moderated', 'visible', 'deleted', 'network', 'uri']; $condition = ['id' => $itemid, 'parent' => [0, $itemid]]; - $item = dba::selectFirst('item', $fields, $condition); + $item = self::selectFirst($fields, $condition); if (!DBM::is_result($item)) { return; @@ -1525,38 +1785,36 @@ class Item extends BaseObject return; } - $item = dba::selectFirst('item', [], ['id' => $itemid]); - - if (DBM::is_result($item) && ($item["allow_cid"] == '') && ($item["allow_gid"] == '') && - ($item["deny_cid"] == '') && ($item["deny_gid"] == '')) { - - if (!dba::exists('item', ['uri' => $item['uri'], 'uid' => 0])) { - // Preparing public shadow (removing user specific data) - $item['uid'] = 0; - unset($item['id']); - unset($item['parent']); - unset($item['wall']); - unset($item['mention']); - unset($item['origin']); - unset($item['starred']); - unset($item['rendered-hash']); - unset($item['rendered-html']); - if ($item['uri'] == $item['parent-uri']) { - $item['contact-id'] = Contact::getIdForURL($item['owner-link']); - } else { - $item['contact-id'] = Contact::getIdForURL($item['author-link']); - } + if (self::exists(['uri' => $item['uri'], 'uid' => 0])) { + return; + } - if (in_array($item['type'], ["net-comment", "wall-comment"])) { - $item['type'] = 'remote-comment'; - } elseif ($item['type'] == 'wall') { - $item['type'] = 'remote'; - } + $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]); - $public_shadow = self::insert($item, false, false, true); + if (DBM::is_result($item)) { + // Preparing public shadow (removing user specific data) + $item['uid'] = 0; + unset($item['id']); + unset($item['parent']); + unset($item['wall']); + unset($item['mention']); + unset($item['origin']); + unset($item['starred']); + if ($item['uri'] == $item['parent-uri']) { + $item['contact-id'] = $item['owner-id']; + } else { + $item['contact-id'] = $item['author-id']; + } - logger("Stored public shadow for thread ".$itemid." under id ".$public_shadow, LOGGER_DEBUG); + if (in_array($item['type'], ["net-comment", "wall-comment"])) { + $item['type'] = 'remote-comment'; + } elseif ($item['type'] == 'wall') { + $item['type'] = 'remote'; } + + $public_shadow = self::insert($item, false, false, true); + + logger("Stored public shadow for thread ".$itemid." under id ".$public_shadow, LOGGER_DEBUG); } } @@ -1569,7 +1827,7 @@ class Item extends BaseObject */ public static function addShadowPost($itemid) { - $item = dba::selectFirst('item', [], ['id' => $itemid]); + $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]); if (!DBM::is_result($item)) { return; } @@ -1586,12 +1844,12 @@ class Item extends BaseObject } // Is there a shadow parent? - if (!dba::exists('item', ['uri' => $item['parent-uri'], 'uid' => 0])) { + if (!self::exists(['uri' => $item['parent-uri'], 'uid' => 0])) { return; } // Is there already a shadow entry? - if (dba::exists('item', ['uri' => $item['uri'], 'uid' => 0])) { + if (self::exists(['uri' => $item['uri'], 'uid' => 0])) { return; } @@ -1607,8 +1865,6 @@ class Item extends BaseObject unset($item['mention']); unset($item['origin']); unset($item['starred']); - unset($item['rendered-hash']); - unset($item['rendered-html']); $item['contact-id'] = Contact::getIdForURL($item['author-link']); if (in_array($item['type'], ["net-comment", "wall-comment"])) { @@ -1623,45 +1879,25 @@ class Item extends BaseObject // If this was a comment to a Diaspora post we don't get our comment back. // This means that we have to distribute the comment by ourselves. - if ($origin && dba::exists('item', ['id' => $parent, 'network' => NETWORK_DIASPORA])) { + if ($origin && self::exists(['id' => $parent, 'network' => NETWORK_DIASPORA])) { self::distribute($public_shadow); } } /** - * Adds a "lang" specification in a "postopts" element of given $arr, - * if possible and not already present. + * Adds a language specification in a "language" element of given $arr. * Expects "body" element to exist in $arr. */ - private static function addLanguageInPostopts(&$item) + private static function addLanguageToItemArray(&$item) { - $postopts = ""; - - if (!empty($item['postopts'])) { - if (strstr($item['postopts'], 'lang=')) { - // do not override - return; - } - $postopts = $item['postopts']; - } - $naked_body = Text\BBCode::toPlaintext($item['body'], false); - $languages = (new Text_LanguageDetect())->detect($naked_body, 3); - - if (sizeof($languages) > 0) { - if ($postopts != '') { - $postopts .= '&'; // arbitrary separator, to be reviewed - } - - $postopts .= 'lang='; - $sep = ""; + $ld = new Text_LanguageDetect(); + $ld->setNameMode(2); + $languages = $ld->detect($naked_body, 3); - foreach ($languages as $language => $score) { - $postopts .= $sep . $language . ";" . $score; - $sep = ':'; - } - $item['postopts'] = $postopts; + if (is_array($languages)) { + $item['language'] = json_encode($languages); } } @@ -1834,7 +2070,7 @@ class Item extends BaseObject public static function getGuidById($id) { - $item = dba::selectFirst('item', ['guid'], ['id' => $id]); + $item = self::selectFirst(['guid'], ['id' => $id]); if (DBM::is_result($item)) { return $item['guid']; } else { @@ -1868,8 +2104,6 @@ class Item extends BaseObject $item = dba::fetch_first("SELECT `item`.`id`, `user`.`nickname` FROM `item` INNER JOIN `user` ON `user`.`uid` = `item`.`uid` WHERE `item`.`visible` AND NOT `item`.`deleted` AND NOT `item`.`moderated` - AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = '' - AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = '' AND NOT `item`.`private` AND `item`.`wall` AND `item`.`guid` = ?", $guid); if (DBM::is_result($item)) { @@ -1898,7 +2132,7 @@ class Item extends BaseObject $community_page = (($user['page-flags'] == PAGE_COMMUNITY) ? true : false); $prvgroup = (($user['page-flags'] == PAGE_PRVGROUP) ? true : false); - $item = dba::selectFirst('item', [], ['id' => $item_id]); + $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $item_id]); if (!DBM::is_result($item)) { return; } @@ -2171,17 +2405,8 @@ class Item extends BaseObject private static function hasPermissions($obj) { - return ( - ( - x($obj, 'allow_cid') - ) || ( - x($obj, 'allow_gid') - ) || ( - x($obj, 'deny_cid') - ) || ( - x($obj, 'deny_gid') - ) - ); + return !empty($obj['allow_cid']) || !empty($obj['allow_gid']) || + !empty($obj['deny_cid']) || !empty($obj['deny_gid']); } private static function samePermissions($obj1, $obj2) @@ -2247,74 +2472,76 @@ class Item extends BaseObject return; } + $condition = ["`uid` = ? AND NOT `deleted` AND `id` = `parent` AND `gravity` = ?", + $uid, GRAVITY_PARENT]; + /* * $expire_network_only = save your own wall posts * and just expire conversations started by others */ - $expire_network_only = PConfig::get($uid,'expire', 'network_only'); - $sql_extra = (intval($expire_network_only) ? " AND wall = 0 " : ""); + $expire_network_only = PConfig::get($uid, 'expire', 'network_only', false); + + if ($expire_network_only) { + $condition[0] .= " AND NOT `wall`"; + } if ($network != "") { - $sql_extra .= sprintf(" AND network = '%s' ", dbesc($network)); + $condition[0] .= " AND `network` = ?"; + $condition[] = $network; /* * There is an index "uid_network_received" but not "uid_network_created" * This avoids the creation of another index just for one purpose. * And it doesn't really matter wether to look at "received" or "created" */ - $range = "AND `received` < UTC_TIMESTAMP() - INTERVAL %d DAY "; + $condition[0] .= " AND `received` < UTC_TIMESTAMP() - INTERVAL ? DAY"; + $condition[] = $days; } else { - $range = "AND `created` < UTC_TIMESTAMP() - INTERVAL %d DAY "; + $condition[0] .= " AND `created` < UTC_TIMESTAMP() - INTERVAL ? DAY"; + $condition[] = $days; } - $r = q("SELECT `file`, `resource-id`, `starred`, `type`, `id` FROM `item` - WHERE `uid` = %d $range - AND `id` = `parent` - $sql_extra - AND `deleted` = 0", - intval($uid), - intval($days) - ); + $items = self::select(['file', 'resource-id', 'starred', 'type', 'id'], $condition); - if (!DBM::is_result($r)) { + if (!DBM::is_result($items)) { return; } - $expire_items = PConfig::get($uid, 'expire', 'items', 1); + $expire_items = PConfig::get($uid, 'expire', 'items', true); // Forcing expiring of items - but not notes and marked items if ($force) { $expire_items = true; } - $expire_notes = PConfig::get($uid, 'expire', 'notes', 1); - $expire_starred = PConfig::get($uid, 'expire', 'starred', 1); - $expire_photos = PConfig::get($uid, 'expire', 'photos', 0); - - logger('User '.$uid.': expire: # items=' . count($r). "; expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos"); + $expire_notes = PConfig::get($uid, 'expire', 'notes', true); + $expire_starred = PConfig::get($uid, 'expire', 'starred', true); + $expire_photos = PConfig::get($uid, 'expire', 'photos', false); - foreach ($r as $item) { + logger('User ' . $uid . ": expire items: $expire_items, expire notes: $expire_notes, expire starred: $expire_starred, expire photos: $expire_photos"); + while ($item = Item::fetch($items)) { // don't expire filed items - if (strpos($item['file'],'[') !== false) { + if (strpos($item['file'], '[') !== false) { continue; } // Only expire posts, not photos and photo comments - if ($expire_photos == 0 && strlen($item['resource-id'])) { + if (!$expire_photos && strlen($item['resource-id'])) { continue; - } elseif ($expire_starred == 0 && intval($item['starred'])) { + } elseif (!$expire_starred && intval($item['starred'])) { continue; - } elseif ($expire_notes == 0 && $item['type'] == 'note') { + } elseif (!$expire_notes && $item['type'] == 'note') { continue; - } elseif ($expire_items == 0 && $item['type'] != 'note') { + } elseif (!$expire_items && $item['type'] != 'note') { continue; } self::deleteById($item['id'], PRIORITY_LOW); } + dba::close($items); } public static function firstPostDate($uid, $wall = false) @@ -2384,7 +2611,7 @@ class Item extends BaseObject logger('like: verb ' . $verb . ' item ' . $item_id); - $item = dba::selectFirst('item', [], ['`id` = ? OR `uri` = ?', $item_id, $item_id]); + $item = self::selectFirst(self::ITEM_FIELDLIST, ['`id` = ? OR `uri` = ?', $item_id, $item_id]); if (!DBM::is_result($item)) { logger('like: unknown item ' . $item_id); return false; @@ -2433,30 +2660,32 @@ class Item extends BaseObject // event participation are essentially radio toggles. If you make a subsequent choice, // we need to eradicate your first choice. if ($event_verb_flag) { - $verbs = "'" . dbesc(ACTIVITY_ATTEND) . "', '" . dbesc(ACTIVITY_ATTENDNO) . "', '" . dbesc(ACTIVITY_ATTENDMAYBE) . "'"; + $verbs = [ACTIVITY_ATTEND, ACTIVITY_ATTENDNO, ACTIVITY_ATTENDMAYBE]; } else { - $verbs = "'".dbesc($activity)."'"; - } - - /// @todo This query is expected to be a performance eater due to the "OR" - it has to be changed totally - $existing_like = q("SELECT `id`, `guid`, `verb` FROM `item` - WHERE `verb` IN ($verbs) - AND `deleted` = 0 - AND `author-id` = %d - AND `uid` = %d - AND (`parent` = '%s' OR `parent-uri` = '%s' OR `thr-parent` = '%s') - LIMIT 1", - intval($author_contact['id']), - intval($item['uid']), - dbesc($item_id), dbesc($item_id), dbesc($item['uri']) - ); + $verbs = $activity; + } - // If it exists, mark it as deleted - if (DBM::is_result($existing_like)) { - $like_item = $existing_like[0]; + $base_condition = ['verb' => $verbs, 'deleted' => false, 'gravity' => GRAVITY_ACTIVITY, + 'author-id' => $author_contact['id'], 'uid' => item['uid']]; + + $condition = array_merge($base_condition, ['parent' => $item_id]); + $like_item = self::selectFirst(['id', 'guid', 'verb'], $condition); + if (!DBM::is_result($like_item)) { + $condition = array_merge($base_condition, ['parent-uri' => $item_id]); + $like_item = self::selectFirst(['id', 'guid', 'verb'], $condition); + } + + if (!DBM::is_result($like_item)) { + $condition = array_merge($base_condition, ['thr-parent' => $item_id]); + $like_item = self::selectFirst(['id', 'guid', 'verb'], $condition); + } + + // If it exists, mark it as deleted + if (DBM::is_result($like_item)) { // Already voted, undo it $fields = ['deleted' => true, 'unseen' => true, 'changed' => DateTimeFormat::utcNow()]; + /// @todo Consider using self::update - but before doing so, check the side effects dba::update('item', $fields, ['id' => $like_item['id']]); // Clean up the Diaspora signatures for this like @@ -2510,7 +2739,7 @@ EOT; 'type' => 'activity', 'wall' => $item['wall'], 'origin' => 1, - 'gravity' => GRAVITY_LIKE, + 'gravity' => GRAVITY_ACTIVITY, 'parent' => $item['id'], 'parent-uri' => $item['uri'], 'thr-parent' => $item['uri'], @@ -2559,7 +2788,7 @@ EOT; 'moderated', 'visible', 'starred', 'bookmark', 'contact-id', 'deleted', 'origin', 'forum_mode', 'mention', 'network', 'author-id', 'owner-id']; $condition = ["`id` = ? AND (`parent` = ? OR `parent` = 0)", $itemid, $itemid]; - $item = dba::selectFirst('item', $fields, $condition); + $item = self::selectFirst($fields, $condition); if (!DBM::is_result($item)) { return; @@ -2576,12 +2805,12 @@ EOT; private static function updateThread($itemid, $setmention = false) { - $fields = ['uid', 'guid', 'title', 'body', 'created', 'edited', 'commented', 'received', 'changed', + $fields = ['uid', 'guid', 'created', 'edited', 'commented', 'received', 'changed', 'wall', 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark', 'contact-id', - 'deleted', 'origin', 'forum_mode', 'network', 'author-id', 'owner-id', 'rendered-html', 'rendered-hash']; + 'deleted', 'origin', 'forum_mode', 'network', 'author-id', 'owner-id']; $condition = ["`id` = ? AND (`parent` = ? OR `parent` = 0)", $itemid, $itemid]; - $item = dba::selectFirst('item', $fields, $condition); + $item = self::selectFirst($fields, $condition); if (!DBM::is_result($item)) { return; } @@ -2595,7 +2824,7 @@ EOT; $fields = []; foreach ($item as $field => $data) { - if (!in_array($field, ["guid", "title", "body", "rendered-html", "rendered-hash"])) { + if (!in_array($field, ["guid"])) { $fields[$field] = $data; } } @@ -2603,19 +2832,6 @@ EOT; $result = dba::update('thread', $fields, ['iid' => $itemid]); logger("Update thread for item ".$itemid." - guid ".$item["guid"]." - ".(int)$result, LOGGER_DEBUG); - - // Updating a shadow item entry - $items = dba::selectFirst('item', ['id'], ['guid' => $item['guid'], 'uid' => 0]); - - if (!DBM::is_result($items)) { - return; - } - - $fields = ['title' => $item['title'], 'body' => $item['body'], - 'rendered-html' => $item['rendered-html'], 'rendered-hash' => $item['rendered-hash']]; - $result = dba::update('item', $fields, ['id' => $items['id']]); - - logger("Updating public shadow for post ".$items["id"]." - guid ".$item["guid"]." Result: ".print_r($result, true), LOGGER_DEBUG); } private static function deleteThread($itemid, $itemuri = "") @@ -2633,7 +2849,7 @@ EOT; if ($itemuri != "") { $condition = ["`uri` = ? AND NOT `deleted` AND NOT (`uid` IN (?, 0))", $itemuri, $item["uid"]]; - if (!dba::exists('item', $condition)) { + if (!self::exists($condition)) { dba::delete('item', ['uri' => $itemuri, 'uid' => 0]); logger("deleteThread: Deleted shadow for item ".$itemuri, LOGGER_DEBUG); }