+ // 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', 'iaid',
+ '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'];
+
+ // 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];
+
+ /**
+ * @brief returns an activity index from an activity string
+ *
+ * @param string $activity activity string
+ * @return integer Activity index
+ */
+ private static function activityToIndex($activity)
+ {
+ $index = array_search($activity, self::ACTIVITIES);
+
+ if (is_bool($index)) {
+ $index = -1;
+ }
+
+ return $index;
+ }
+
+ /**
+ * @brief returns an activity string from an activity index
+ *
+ * @param integer $index activity index
+ * @return string Activity string
+ */
+ private static function indexToActivity($index)
+ {
+ if (!isset(self::ACTIVITIES[$index])) {
+ return '';
+ }
+
+ return self::ACTIVITIES[$index];
+ }
+
+ /**
+ * @brief Fetch a single item row
+ *
+ * @param mixed $stmt statement object
+ * @return array current row
+ */
+ public static function fetch($stmt)
+ {
+ $row = dba::fetch($stmt);
+
+ if (is_bool($row)) {
+ return $row;
+ }
+
+ // ---------------------- Transform item structure data ----------------------
+
+ // 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'];
+ }
+ }
+
+ // We can always comment on posts from these networks
+ if (array_key_exists('writable', $row) &&
+ in_array($row['internal-network'], [NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS])) {
+ $row['writable'] = true;
+ }
+
+ // ---------------------- Transform item content data ----------------------
+
+ // 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['internal-item-' . $field])) {
+ $row[$field] = $row['internal-item-' . $field];
+ }
+ unset($row['internal-item-' . $field]);
+ }
+
+ if (!empty($row['internal-iaid']) && array_key_exists('verb', $row)) {
+ $row['verb'] = self::indexToActivity($row['internal-activity']);
+ if (array_key_exists('title', $row)) {
+ $row['title'] = '';
+ }
+ if (array_key_exists('body', $row)) {
+ $row['body'] = $row['verb'];
+ }
+ if (array_key_exists('object', $row)) {
+ $row['object'] = '';
+ }
+ if (array_key_exists('object-type', $row)) {
+ $row['object-type'] = ACTIVITY_OBJ_NOTE;
+ }
+ } elseif (in_array($row['verb'], ['', ACTIVITY_POST, ACTIVITY_SHARE])) {
+ // Posts don't have an object or target - but having tags or files.
+ // We safe some performance by building tag and file strings only here.
+ // We remove object and target since they aren't used for this type.
+ if (array_key_exists('object', $row)) {
+ $row['object'] = '';
+ }
+ if (array_key_exists('target', $row)) {
+ $row['target'] = '';
+ }
+ // Build the tag string out of the term entries
+ if (array_key_exists('tag', $row)) {
+ $row['tag'] = Term::tagTextFromItemId($row['internal-iid']);
+ }
+
+ // Build the file string out of the term entries
+ if (array_key_exists('file', $row)) {
+ $row['file'] = Term::fileTextFromItemId($row['internal-iid']);
+ }
+ }
+
+ // Remove internal fields
+ unset($row['internal-activity']);
+ unset($row['internal-network']);
+ unset($row['internal-iid']);
+ unset($row['internal-iaid']);
+ unset($row['internal-icid']);
+
+ return $row;
+ }
+
+ /**
+ * @brief Fills an array with data from an item query
+ *
+ * @param object $stmt statement object
+ * @return array Data array
+ */
+ public static function inArray($stmt, $do_close = true) {
+ if (is_bool($stmt)) {
+ return $stmt;
+ }
+
+ $data = [];
+ while ($row = self::fetch($stmt)) {
+ $data[] = $row;
+ }
+ if ($do_close) {
+ dba::close($stmt);
+ }
+ 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;
+ }
+