52d67b0f44
Try to also employ an id when possible. Involves reworking some of the indices.
950 lines
30 KiB
PHP
950 lines
30 KiB
PHP
<?php
|
|
// This file is part of GNU social - https://www.gnu.org/software/social
|
|
//
|
|
// GNU social is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// GNU social is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
/**
|
|
* @category Notices
|
|
* @package GNUsocial
|
|
* @author Shashi Gowda <connect2shashi@gmail.com>
|
|
* @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
|
|
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
|
*/
|
|
|
|
defined('GNUSOCIAL') || die();
|
|
|
|
class Profile_list extends Managed_DataObject
|
|
{
|
|
public $__table = 'profile_list'; // table name
|
|
public $id; // int(4) primary_key not_null
|
|
public $tagger; // int(4)
|
|
public $tag; // varchar(64)
|
|
public $description; // text
|
|
public $private; // bool default_false
|
|
public $created; // datetime()
|
|
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
|
|
public $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space
|
|
public $mainpage; // varchar(191) not 255 because utf8mb4 takes more space
|
|
public $tagged_count; // smallint
|
|
public $subscriber_count; // smallint
|
|
|
|
public static function schemaDef()
|
|
{
|
|
return array(
|
|
'fields' => array(
|
|
'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
|
|
'tagger' => array('type' => 'int', 'not null' => true, 'description' => 'user making the tag'),
|
|
'tag' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'people tag'),
|
|
'description' => array('type' => 'text', 'description' => 'description of the people tag'),
|
|
'private' => array('type' => 'bool', 'default' => false, 'description' => 'is this tag private'),
|
|
|
|
'created' => array('type' => 'datetime', 'description' => 'date the tag was added'),
|
|
'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date the tag was modified'),
|
|
|
|
'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universal identifier'),
|
|
'mainpage' => array('type' => 'varchar', 'length' => 191, 'description' => 'page to link to'),
|
|
'tagged_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of people tagged with this tag by this user'),
|
|
'subscriber_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of subscribers to this tag'),
|
|
),
|
|
'primary key' => array('tagger', 'tag'),
|
|
'unique keys' => array(
|
|
'profile_list_id_key' => array('id'),
|
|
),
|
|
'foreign keys' => array(
|
|
'profile_list_tagger_fkey' => array('profile', array('tagger' => 'id')),
|
|
),
|
|
'indexes' => array(
|
|
'profile_list_modified_id_idx' => array('modified', 'id'),
|
|
'profile_list_tag_idx' => array('tag'),
|
|
'profile_list_tagged_count_idx' => array('tagged_count'),
|
|
'profile_list_subscriber_count_idx' => array('subscriber_count'),
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* get the tagger of this profile_list object
|
|
*
|
|
* @return Profile the tagger
|
|
*/
|
|
|
|
public function getTagger()
|
|
{
|
|
return Profile::getByID($this->tagger);
|
|
}
|
|
|
|
/**
|
|
* return a string to identify this
|
|
* profile_list in the user interface etc.
|
|
*
|
|
* @return String
|
|
*/
|
|
|
|
public function getBestName()
|
|
{
|
|
return $this->tag;
|
|
}
|
|
|
|
/**
|
|
* return a uri string for this profile_list
|
|
*
|
|
* @return String uri
|
|
*/
|
|
|
|
public function getUri()
|
|
{
|
|
$uri = null;
|
|
if (Event::handle('StartProfiletagGetUri', array($this, &$uri))) {
|
|
if (!empty($this->uri)) {
|
|
$uri = $this->uri;
|
|
} else {
|
|
$uri = common_local_url(
|
|
'profiletagbyid',
|
|
['id' => $this->id, 'tagger_id' => $this->tagger]
|
|
);
|
|
}
|
|
}
|
|
Event::handle('EndProfiletagGetUri', array($this, &$uri));
|
|
return $uri;
|
|
}
|
|
|
|
/**
|
|
* return a url to the homepage of this item
|
|
*
|
|
* @return String home url
|
|
*/
|
|
|
|
public function homeUrl()
|
|
{
|
|
$url = null;
|
|
if (Event::handle('StartUserPeopletagHomeUrl', array($this, &$url))) {
|
|
// normally stored in mainpage, but older ones may be null
|
|
if (!empty($this->mainpage)) {
|
|
$url = $this->mainpage;
|
|
} else {
|
|
$url = common_local_url(
|
|
'showprofiletag',
|
|
[
|
|
'nickname' => $this->getTagger()->nickname,
|
|
'tag' => $this->tag,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
Event::handle('EndUserPeopletagHomeUrl', array($this, &$url));
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* return an immutable url for this object
|
|
*
|
|
* @return String permalink
|
|
*/
|
|
|
|
public function permalink()
|
|
{
|
|
$url = null;
|
|
if (Event::handle('StartProfiletagPermalink', array($this, &$url))) {
|
|
$url = common_local_url(
|
|
'profiletagbyid',
|
|
['id' => $this->id]
|
|
);
|
|
}
|
|
Event::handle('EndProfiletagPermalink', array($this, &$url));
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Query notices by users associated with this tag,
|
|
* but first check the cache before hitting the DB.
|
|
*
|
|
* @param integer $offset offset
|
|
* @param integer $limit maximum no of results
|
|
* @param integer $since_id=null since this id
|
|
* @param integer $max_id=null maximum id in result
|
|
*
|
|
* @return Notice the query
|
|
*/
|
|
|
|
public function getNotices($offset, $limit, $since_id = null, $max_id = null)
|
|
{
|
|
// FIXME: Use something else than Profile::current() to avoid
|
|
// possible confusion between session user and queue processing.
|
|
$stream = new PeopletagNoticeStream($this, Profile::current());
|
|
|
|
return $stream->getNotices($offset, $limit, $since_id, $max_id);
|
|
}
|
|
|
|
/**
|
|
* Get subscribers (local and remote) to this people tag
|
|
* Order by reverse chronology
|
|
*
|
|
* @param integer $offset offset
|
|
* @param integer $limit maximum no of results
|
|
* @param integer $since_id=null since unix timestamp
|
|
* @param integer $upto=null maximum unix timestamp when subscription was made
|
|
*
|
|
* @return Profile results
|
|
*/
|
|
|
|
public function getSubscribers(int $offset = 0, ?int $limit = null, int $since = 0, int $upto = 0)
|
|
{
|
|
$subs = new Profile();
|
|
|
|
$subs->joinAdd(
|
|
array('id', 'profile_tag_subscription:profile_id')
|
|
);
|
|
$subs->whereAdd('profile_tag_subscription.profile_tag_id = ' . $this->id);
|
|
|
|
if (common_config('db', 'type') !== 'mysql') {
|
|
$subs->selectAdd(sprintf(
|
|
'((EXTRACT(DAY %1$s) * 24 + EXTRACT(HOUR %1$s)) * 60 + ' .
|
|
'EXTRACT(MINUTE %1$s)) * 60 + FLOOR(EXTRACT(SECOND %1$s)) AS "cursor"',
|
|
"FROM (profile_tag_subscription.created - TIMESTAMP '1970-01-01 00:00:00')"
|
|
));
|
|
} else {
|
|
$subs->selectAdd("timestampdiff(SECOND, '1970-01-01', profile_tag_subscription.created) AS `cursor`");
|
|
}
|
|
|
|
if ($since != 0) {
|
|
$subs->whereAdd('cursor > ' . $since);
|
|
}
|
|
|
|
if ($upto != 0) {
|
|
$subs->whereAdd('cursor <= ' . $upto);
|
|
}
|
|
|
|
if (!is_null($limit)) {
|
|
$subs->limit($offset, $limit);
|
|
}
|
|
|
|
$subs->orderBy('profile_tag_subscription.created DESC, profile.id DESC');
|
|
$subs->find();
|
|
|
|
return $subs;
|
|
}
|
|
|
|
/**
|
|
* Get all and only local subscribers to this people tag
|
|
* used for distributing notices to user inboxes.
|
|
*
|
|
* @return array ids of users
|
|
*/
|
|
|
|
public function getUserSubscribers()
|
|
{
|
|
// XXX: cache this
|
|
|
|
$user = new User();
|
|
|
|
$user->query(sprintf(
|
|
'SELECT id ' .
|
|
'FROM %1$s INNER JOIN profile_tag_subscription ' .
|
|
'ON %1$s.id = profile_tag_subscription.profile_id ' .
|
|
'WHERE profile_tag_subscription.profile_tag_id = %2$d ',
|
|
$user->escapedTableName(),
|
|
$this->id
|
|
));
|
|
|
|
$ids = [];
|
|
|
|
while ($user->fetch()) {
|
|
$ids[] = $user->id;
|
|
}
|
|
|
|
$user->free();
|
|
|
|
return $ids;
|
|
}
|
|
|
|
/**
|
|
* Check to see if a given profile has
|
|
* subscribed to this people tag's timeline
|
|
*
|
|
* @param mixed $id User or Profile object or integer id
|
|
*
|
|
* @return boolean subscription status
|
|
*/
|
|
|
|
public function hasSubscriber($id)
|
|
{
|
|
if (!is_numeric($id)) {
|
|
$id = $id->id;
|
|
}
|
|
|
|
$sub = Profile_tag_subscription::pkeyGet(array('profile_tag_id' => $this->id,
|
|
'profile_id' => $id));
|
|
return !empty($sub);
|
|
}
|
|
|
|
/**
|
|
* Get profiles tagged with this people tag,
|
|
* include modified timestamp as a "cursor" field
|
|
* order by descending order of modified time
|
|
*
|
|
* @param integer $offset offset
|
|
* @param integer $limit maximum no of results
|
|
* @param integer $since_id=null since unix timestamp
|
|
* @param integer $upto=null maximum unix timestamp when subscription was made
|
|
*
|
|
* @return Profile results
|
|
*/
|
|
|
|
public function getTagged(int $offset = 0, ?int $limit = null, int $since = 0, int $upto = 0)
|
|
{
|
|
$tagged = new Profile();
|
|
$tagged->joinAdd(['id', 'profile_tag:tagged']);
|
|
|
|
if (common_config('db', 'type') !== 'mysql') {
|
|
$tagged->selectAdd(sprintf(
|
|
'((EXTRACT(DAY %1$s) * 24 + EXTRACT(HOUR %1$s)) * 60 + ' .
|
|
'EXTRACT(MINUTE %1$s)) * 60 + FLOOR(EXTRACT(SECOND %1$s)) AS "cursor"',
|
|
"FROM (profile_tag.modified - TIMESTAMP '1970-01-01 00:00:00')"
|
|
));
|
|
} else {
|
|
$tagged->selectAdd("timestampdiff(SECOND, '1970-01-01', profile_tag.modified) AS `cursor`");
|
|
}
|
|
|
|
$tagged->whereAdd('profile_tag.tagger = '.$this->tagger);
|
|
$tagged->whereAdd("profile_tag.tag = '{$this->tag}'");
|
|
|
|
if ($since != 0) {
|
|
$tagged->whereAdd('cursor > ' . $since);
|
|
}
|
|
|
|
if ($upto != 0) {
|
|
$tagged->whereAdd('cursor <= ' . $upto);
|
|
}
|
|
|
|
if (!is_null($limit)) {
|
|
$tagged->limit($offset, $limit);
|
|
}
|
|
|
|
$tagged->orderBy('profile_tag.modified DESC, profile_tag.tagged DESC');
|
|
$tagged->find();
|
|
|
|
return $tagged;
|
|
}
|
|
|
|
/**
|
|
* Gracefully delete one or many people tags
|
|
* along with their members and subscriptions data
|
|
*
|
|
* @return boolean success
|
|
*/
|
|
|
|
public function delete($useWhere = false)
|
|
{
|
|
// force delete one item at a time.
|
|
if (empty($this->id)) {
|
|
$this->find();
|
|
while ($this->fetch()) {
|
|
$this->delete();
|
|
}
|
|
}
|
|
|
|
Profile_tag::cleanup($this);
|
|
Profile_tag_subscription::cleanup($this);
|
|
|
|
self::blow('profile:lists:%d', $this->tagger);
|
|
|
|
return parent::delete($useWhere);
|
|
}
|
|
|
|
/**
|
|
* Update a people tag gracefully
|
|
* also change "tag" fields in profile_tag table
|
|
*
|
|
* @param Profile_list $dataObject Object's original form
|
|
*
|
|
* @return boolean success
|
|
*/
|
|
|
|
public function update($dataObject = false)
|
|
{
|
|
if (!is_object($dataObject) && !$dataObject instanceof Profile_list) {
|
|
return parent::update($dataObject);
|
|
}
|
|
|
|
$result = true;
|
|
|
|
// if original tag was different
|
|
// check to see if the new tag already exists
|
|
// if not, rename the tag correctly
|
|
if ($dataObject->tag != $this->tag || $dataObject->tagger != $this->tagger) {
|
|
$existing = Profile_list::getByTaggerAndTag($this->tagger, $this->tag);
|
|
if (!empty($existing)) {
|
|
// TRANS: Server exception.
|
|
throw new ServerException(_('The tag you are trying to rename ' .
|
|
'to already exists.'));
|
|
}
|
|
// move the tag
|
|
// XXX: allow OStatus plugin to send out profile tag
|
|
$result = Profile_tag::moveTag($dataObject, $this);
|
|
}
|
|
return parent::update($dataObject);
|
|
}
|
|
|
|
/**
|
|
* return an xml string representing this people tag
|
|
* as the author of an atom feed
|
|
*
|
|
* @return string atom author element
|
|
*/
|
|
|
|
public function asAtomAuthor()
|
|
{
|
|
$xs = new XMLStringer(true);
|
|
|
|
$tagger = $this->getTagger();
|
|
$xs->elementStart('author');
|
|
$xs->element('name', null, '@' . $tagger->nickname . '/' . $this->tag);
|
|
$xs->element('uri', null, $this->permalink());
|
|
$xs->elementEnd('author');
|
|
|
|
return $xs->getString();
|
|
}
|
|
|
|
/**
|
|
* return an xml string to represent this people tag
|
|
* as a noun in an activitystreams feed.
|
|
*
|
|
* @param string $element the xml tag
|
|
*
|
|
* @return string activitystreams noun
|
|
*/
|
|
|
|
public function asActivityNoun($element)
|
|
{
|
|
$noun = ActivityObject::fromPeopletag($this);
|
|
return $noun->asString('activity:' . $element);
|
|
}
|
|
|
|
/**
|
|
* get the cached number of profiles tagged with this
|
|
* people tag, re-count if the argument is true.
|
|
*
|
|
* @param boolean $recount whether to ignore cache
|
|
*
|
|
* @return integer count
|
|
*/
|
|
|
|
public function taggedCount($recount = false)
|
|
{
|
|
$keypart = sprintf(
|
|
'profile_list:tagged_count:%d:%s',
|
|
$this->tagger,
|
|
$this->tag
|
|
);
|
|
|
|
$count = self::cacheGet($keypart);
|
|
|
|
if ($count === false) {
|
|
$tags = new Profile_tag();
|
|
|
|
$tags->tag = $this->tag;
|
|
$tags->tagger = $this->tagger;
|
|
|
|
$count = $tags->count('distinct tagged');
|
|
|
|
self::cacheSet($keypart, $count);
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* get the cached number of profiles subscribed to this
|
|
* people tag, re-count if the argument is true.
|
|
*
|
|
* @param boolean $recount whether to ignore cache
|
|
*
|
|
* @return integer count
|
|
*/
|
|
|
|
public function subscriberCount($recount = false)
|
|
{
|
|
$keypart = sprintf(
|
|
'profile_list:subscriber_count:%d',
|
|
$this->id
|
|
);
|
|
|
|
$count = self::cacheGet($keypart);
|
|
|
|
if ($count === false) {
|
|
$sub = new Profile_tag_subscription();
|
|
$sub->profile_tag_id = $this->id;
|
|
$count = (int) $sub->count('distinct profile_id');
|
|
|
|
self::cacheSet($keypart, $count);
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* get the cached number of profiles subscribed to this
|
|
* people tag, re-count if the argument is true.
|
|
*
|
|
* @param boolean $recount whether to ignore cache
|
|
*
|
|
* @return integer count
|
|
*/
|
|
|
|
public function blowNoticeStreamCache($all = false)
|
|
{
|
|
self::blow('profile_list:notice_ids:%d', $this->id);
|
|
if ($all) {
|
|
self::blow('profile_list:notice_ids:%d;last', $this->id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* get the Profile_list object by the
|
|
* given tagger and with given tag
|
|
*
|
|
* @param integer $tagger the id of the creator profile
|
|
* @param integer $tag the tag
|
|
*
|
|
* @return integer count
|
|
*/
|
|
|
|
public static function getByTaggerAndTag($tagger, $tag)
|
|
{
|
|
$ptag = Profile_list::pkeyGet(array('tagger' => $tagger, 'tag' => $tag));
|
|
return $ptag;
|
|
}
|
|
|
|
/**
|
|
* create a profile_list record for a tag, tagger pair
|
|
* if it doesn't exist, return it.
|
|
*
|
|
* @param integer $tagger the tagger
|
|
* @param string $tag the tag
|
|
* @param string $description description
|
|
* @param boolean $private protected or not
|
|
*
|
|
* @return Profile_list the people tag object
|
|
*/
|
|
|
|
public static function ensureTag($tagger, $tag, $description = null, $private = false)
|
|
{
|
|
$ptag = Profile_list::getByTaggerAndTag($tagger, $tag);
|
|
|
|
if (empty($ptag->id)) {
|
|
$args = array(
|
|
'tag' => $tag,
|
|
'tagger' => $tagger,
|
|
'description' => $description,
|
|
'private' => $private
|
|
);
|
|
|
|
$new_tag = Profile_list::saveNew($args);
|
|
|
|
return $new_tag;
|
|
}
|
|
return $ptag;
|
|
}
|
|
|
|
/**
|
|
* get the maximum number of characters
|
|
* that can be used in the description of
|
|
* a people tag.
|
|
*
|
|
* determined by $config['peopletag']['desclimit']
|
|
* if not set, falls back to $config['site']['textlimit']
|
|
*
|
|
* @return integer maximum number of characters
|
|
*/
|
|
|
|
public static function maxDescription()
|
|
{
|
|
$desclimit = common_config('peopletag', 'desclimit');
|
|
// null => use global limit (distinct from 0!)
|
|
if (is_null($desclimit)) {
|
|
$desclimit = common_config('site', 'textlimit');
|
|
}
|
|
return $desclimit;
|
|
}
|
|
|
|
/**
|
|
* check if the length of given text exceeds
|
|
* character limit.
|
|
*
|
|
* @param string $desc the description
|
|
*
|
|
* @return boolean is the descripition too long?
|
|
*/
|
|
|
|
public static function descriptionTooLong($desc)
|
|
{
|
|
$desclimit = self::maxDescription();
|
|
return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
|
|
}
|
|
|
|
/**
|
|
* save a new people tag, this should be always used
|
|
* since it makes uri, homeurl, created and modified
|
|
* timestamps and performs checks.
|
|
*
|
|
* @param array $fields an array with fields and their values
|
|
*
|
|
* @return mixed Profile_list on success, false on fail
|
|
*/
|
|
public static function saveNew(array $fields)
|
|
{
|
|
extract($fields);
|
|
|
|
$ptag = new Profile_list();
|
|
|
|
$ptag->query('START TRANSACTION');
|
|
|
|
if (empty($tagger)) {
|
|
// TRANS: Server exception saving new tag without having a tagger specified.
|
|
throw new Exception(_('No tagger specified.'));
|
|
}
|
|
|
|
if (empty($tag)) {
|
|
// TRANS: Server exception saving new tag without having a tag specified.
|
|
throw new Exception(_('No tag specified.'));
|
|
}
|
|
|
|
if (empty($mainpage)) {
|
|
$mainpage = null;
|
|
}
|
|
|
|
if (empty($uri)) {
|
|
// fill in later...
|
|
$uri = null;
|
|
}
|
|
|
|
if (empty($mainpage)) {
|
|
$mainpage = null;
|
|
}
|
|
|
|
if (empty($description)) {
|
|
$description = null;
|
|
}
|
|
|
|
if (empty($private)) {
|
|
$private = false;
|
|
}
|
|
|
|
$ptag->tagger = $tagger;
|
|
$ptag->tag = $tag;
|
|
$ptag->description = $description;
|
|
$ptag->private = $private;
|
|
$ptag->uri = $uri;
|
|
$ptag->mainpage = $mainpage;
|
|
$ptag->created = common_sql_now();
|
|
$ptag->modified = common_sql_now();
|
|
|
|
$result = $ptag->insert();
|
|
|
|
if (!$result) {
|
|
common_log_db_error($ptag, 'INSERT', __FILE__);
|
|
// TRANS: Server exception saving new tag.
|
|
throw new ServerException(_('Could not create profile tag.'));
|
|
}
|
|
|
|
if (!isset($uri) || empty($uri)) {
|
|
$orig = clone($ptag);
|
|
$ptag->uri = common_local_url('profiletagbyid', array('id' => $ptag->id, 'tagger_id' => $ptag->tagger));
|
|
$result = $ptag->update($orig);
|
|
if (!$result) {
|
|
common_log_db_error($ptag, 'UPDATE', __FILE__);
|
|
// TRANS: Server exception saving new tag.
|
|
throw new ServerException(_('Could not set profile tag URI.'));
|
|
}
|
|
}
|
|
|
|
if (!isset($mainpage) || empty($mainpage)) {
|
|
$orig = clone($ptag);
|
|
$user = User::getKV('id', $ptag->tagger);
|
|
if (!empty($user)) {
|
|
$ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'nickname' => $user->getNickname()));
|
|
} else {
|
|
$ptag->mainpage = $uri; // assume this is a remote peopletag and the uri works
|
|
}
|
|
|
|
$result = $ptag->update($orig);
|
|
if (!$result) {
|
|
common_log_db_error($ptag, 'UPDATE', __FILE__);
|
|
// TRANS: Server exception saving new tag.
|
|
throw new ServerException(_('Could not set profile tag mainpage.'));
|
|
}
|
|
}
|
|
return $ptag;
|
|
}
|
|
|
|
/**
|
|
* get all items at given cursor position for api
|
|
*
|
|
* @param callback $fn a function that takes the following arguments in order:
|
|
* $offset, $limit, $since_id, $max_id
|
|
* and returns a Profile_list object after making the DB query
|
|
* @param array $args arguments required for $fn
|
|
* @param integer $cursor the cursor
|
|
* @param integer $count max. number of results
|
|
*
|
|
* Algorithm:
|
|
* - if cursor is 0, return empty list
|
|
* - if cursor is -1, get first 21 items, next_cursor = 20th prev_cursor = 0
|
|
* - if cursor is +ve get 22 consecutive items before starting at cursor
|
|
* - return items[1..20] if items[0] == cursor else return items[0..21]
|
|
* - prev_cursor = items[1]
|
|
* - next_cursor = id of the last item being returned
|
|
*
|
|
* - if cursor is -ve get 22 consecutive items after cursor starting at cursor
|
|
* - return items[1..20]
|
|
*
|
|
* @returns array (array (mixed items), int next_cursor, int previous_cursor)
|
|
*/
|
|
|
|
// XXX: This should be in Memcached_DataObject... eventually
|
|
|
|
public static function getAtCursor($fn, array $args, $cursor, $count = 20)
|
|
{
|
|
$items = array();
|
|
|
|
$since_id = 0;
|
|
$max_id = 0;
|
|
$next_cursor = 0;
|
|
$prev_cursor = 0;
|
|
|
|
if ($cursor > 0) {
|
|
// if cursor is +ve fetch $count+2 items before cursor starting at cursor
|
|
$max_id = $cursor;
|
|
$fn_args = array_merge($args, array(0, $count+2, 0, $max_id));
|
|
$list = call_user_func_array($fn, $fn_args);
|
|
while ($list->fetch()) {
|
|
$items[] = clone($list);
|
|
}
|
|
|
|
if ((isset($items[0]->cursor) && $items[0]->cursor == $cursor) ||
|
|
$items[0]->id == $cursor) {
|
|
array_shift($items);
|
|
$prev_cursor = isset($items[0]->cursor) ?
|
|
-$items[0]->cursor : -$items[0]->id;
|
|
} else {
|
|
if (count($items) > $count+1) {
|
|
array_shift($items);
|
|
}
|
|
// this means the cursor item has been deleted, check to see if there are more
|
|
$fn_args = array_merge($args, array(0, 1, $cursor));
|
|
$more = call_user_func($fn, $fn_args);
|
|
if (!$more->fetch() || empty($more)) {
|
|
// no more items.
|
|
$prev_cursor = 0;
|
|
} else {
|
|
$prev_cursor = isset($items[0]->cursor) ?
|
|
-$items[0]->cursor : -$items[0]->id;
|
|
}
|
|
}
|
|
|
|
if (count($items)==$count+1) {
|
|
// this means there is a next page.
|
|
$next = array_pop($items);
|
|
$next_cursor = isset($next->cursor) ?
|
|
$items[$count-1]->cursor : $items[$count-1]->id;
|
|
}
|
|
} elseif ($cursor < -1) {
|
|
// if cursor is -ve fetch $count+2 items created after -$cursor-1
|
|
$cursor = abs($cursor);
|
|
$since_id = $cursor-1;
|
|
|
|
$fn_args = array_merge($args, array(0, $count+2, $since_id));
|
|
$list = call_user_func_array($fn, $fn_args);
|
|
while ($list->fetch()) {
|
|
$items[] = clone($list);
|
|
}
|
|
|
|
$end = count($items)-1;
|
|
if ((isset($items[$end]->cursor) && $items[$end]->cursor == $cursor) ||
|
|
$items[$end]->id == $cursor) {
|
|
array_pop($items);
|
|
$next_cursor = isset($items[$end-1]->cursor) ?
|
|
$items[$end-1]->cursor : $items[$end-1]->id;
|
|
} else {
|
|
$next_cursor = isset($items[$end]->cursor) ?
|
|
$items[$end]->cursor : $items[$end]->id;
|
|
if ($end > $count) {
|
|
// excess item
|
|
array_pop($items);
|
|
}
|
|
|
|
// check if there are more items for next page
|
|
$fn_args = array_merge($args, array(0, 1, 0, $cursor));
|
|
$more = call_user_func_array($fn, $fn_args);
|
|
if (!$more->fetch() || empty($more)) {
|
|
$next_cursor = 0;
|
|
}
|
|
}
|
|
|
|
if (count($items) == $count+1) {
|
|
// this means there is a previous page.
|
|
$prev = array_shift($items);
|
|
$prev_cursor = isset($prev->cursor) ?
|
|
-$items[0]->cursor : -$items[0]->id;
|
|
}
|
|
} elseif ($cursor == -1) {
|
|
$fn_args = array_merge($args, array(0, $count+1));
|
|
$list = call_user_func_array($fn, $fn_args);
|
|
|
|
while ($list->fetch()) {
|
|
$items[] = clone($list);
|
|
}
|
|
|
|
if (count($items)==$count+1) {
|
|
$next = array_pop($items);
|
|
if (isset($next->cursor)) {
|
|
$next_cursor = $items[$count-1]->cursor;
|
|
} else {
|
|
$next_cursor = $items[$count-1]->id;
|
|
}
|
|
}
|
|
}
|
|
return array($items, $next_cursor, $prev_cursor);
|
|
}
|
|
|
|
/**
|
|
* save a collection of people tags into the cache
|
|
*
|
|
* @param string $ckey cache key
|
|
* @param Profile_list &$tag the results to store
|
|
* @param integer $offset offset for slicing results
|
|
* @param integer $limit maximum number of results
|
|
*
|
|
* @return boolean success
|
|
*/
|
|
|
|
public static function setCache($ckey, &$tag, $offset = 0, $limit = null)
|
|
{
|
|
$cache = Cache::instance();
|
|
if (empty($cache)) {
|
|
return false;
|
|
}
|
|
$str = '';
|
|
$tags = array();
|
|
while ($tag->fetch()) {
|
|
$str .= $tag->tagger . ':' . $tag->tag . ';';
|
|
$tags[] = clone($tag);
|
|
}
|
|
$str = substr($str, 0, -1);
|
|
if ($offset>=0 && !is_null($limit)) {
|
|
$tags = array_slice($tags, $offset, $limit);
|
|
}
|
|
|
|
$tag = new ArrayWrapper($tags);
|
|
|
|
return self::cacheSet($ckey, $str);
|
|
}
|
|
|
|
/**
|
|
* get people tags from the cache
|
|
*
|
|
* @param string $ckey cache key
|
|
* @param integer $offset offset for slicing
|
|
* @param integer $limit limit
|
|
*
|
|
* @return Profile_list results
|
|
*/
|
|
|
|
public static function getCached($ckey, $offset = 0, $limit = null)
|
|
{
|
|
$keys_str = self::cacheGet($ckey);
|
|
if ($keys_str === false) {
|
|
return false;
|
|
}
|
|
|
|
$pairs = explode(';', $keys_str);
|
|
$keys = array();
|
|
foreach ($pairs as $pair) {
|
|
$keys[] = explode(':', $pair);
|
|
}
|
|
|
|
if ($offset>=0 && !is_null($limit)) {
|
|
$keys = array_slice($keys, $offset, $limit);
|
|
}
|
|
return self::getByKeys($keys);
|
|
}
|
|
|
|
/**
|
|
* get Profile_list objects from the database
|
|
* given their (tag, tagger) key pairs.
|
|
*
|
|
* @param array $keys array of array(tagger, tag)
|
|
*
|
|
* @return Profile_list results
|
|
*/
|
|
|
|
public static function getByKeys(array $keys)
|
|
{
|
|
$cache = Cache::instance();
|
|
|
|
if (!empty($cache)) {
|
|
$tags = array();
|
|
|
|
foreach ($keys as $key) {
|
|
$t = Profile_list::getByTaggerAndTag($key[0], $key[1]);
|
|
if (!empty($t)) {
|
|
$tags[] = $t;
|
|
}
|
|
}
|
|
return new ArrayWrapper($tags);
|
|
} else {
|
|
$tag = new Profile_list();
|
|
if (empty($keys)) {
|
|
//if no IDs requested, just return the tag object
|
|
return $tag;
|
|
}
|
|
|
|
$pairs = array();
|
|
foreach ($keys as $key) {
|
|
$pairs[] = '(' . $key[0] . ', "' . $key[1] . '")';
|
|
}
|
|
|
|
$tag->whereAdd('(tagger, tag) in (' . implode(', ', $pairs) . ')');
|
|
|
|
$tag->find();
|
|
|
|
$temp = array();
|
|
|
|
while ($tag->fetch()) {
|
|
$temp[$tag->tagger.'-'.$tag->tag] = clone($tag);
|
|
}
|
|
|
|
$wrapped = array();
|
|
|
|
foreach ($keys as $key) {
|
|
$id = $key[0].'-'.$key[1];
|
|
if (array_key_exists($id, $temp)) {
|
|
$wrapped[] = $temp[$id];
|
|
}
|
|
}
|
|
|
|
return new ArrayWrapper($wrapped);
|
|
}
|
|
}
|
|
|
|
public function insert()
|
|
{
|
|
$result = parent::insert();
|
|
if ($result) {
|
|
self::blow('profile:lists:%d', $this->tagger);
|
|
}
|
|
return $result;
|
|
}
|
|
}
|