3 * StatusNet - the distributed open-source microblogging tool
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 * @author Shashi Gowda <connect2shashi@gmail.com>
21 * @license GNU Affero General Public License http://www.gnu.org/licenses/
24 if (!defined('STATUSNET') && !defined('LACONICA')) {
29 * Table Definition for profile_list
31 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
33 class Profile_list extends Memcached_DataObject
36 /* the code below is auto generated do not remove the above tag */
38 public $__table = 'profile_list'; // table name
39 public $id; // int(4) primary_key not_null
40 public $tagger; // int(4)
41 public $tag; // varchar(64)
42 public $description; // text
43 public $private; // tinyint(1)
44 public $created; // datetime not_null default_0000-00-00%2000%3A00%3A00
45 public $modified; // timestamp not_null default_CURRENT_TIMESTAMP
46 public $uri; // varchar(255) unique_key
47 public $mainpage; // varchar(255)
48 public $tagged_count; // smallint
49 public $subscriber_count; // smallint
52 function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('Profile_list',$k,$v); }
54 /* the code above is auto generated do not remove the tag below */
58 * return a profile_list record, given its tag and tagger.
60 * @param array $kv ideally array('tag' => $tag, 'tagger' => $tagger)
62 * @return Profile_list a Profile_list object with the given tag and tagger.
67 return Memcached_DataObject::pkeyGet('Profile_list', $kv);
71 * get the tagger of this profile_list object
73 * @return Profile the tagger
78 return Profile::staticGet('id', $this->tagger);
82 * return a string to identify this
83 * profile_list in the user interface etc.
88 function getBestName()
94 * return a uri string for this profile_list
102 if (Event::handle('StartProfiletagGetUri', array($this, &$uri))) {
103 if (!empty($this->uri)) {
106 $uri = common_local_url('profiletagbyid',
107 array('id' => $this->id, 'tagger_id' => $this->tagger));
110 Event::handle('EndProfiletagGetUri', array($this, &$uri));
115 * return a url to the homepage of this item
117 * @return String home url
123 if (Event::handle('StartUserPeopletagHomeUrl', array($this, &$url))) {
124 // normally stored in mainpage, but older ones may be null
125 if (!empty($this->mainpage)) {
126 $url = $this->mainpage;
128 $url = common_local_url('showprofiletag',
129 array('tagger' => $this->getTagger()->nickname,
130 'tag' => $this->tag));
133 Event::handle('EndUserPeopletagHomeUrl', array($this, &$url));
138 * return an immutable url for this object
140 * @return String permalink
146 if (Event::handle('StartProfiletagPermalink', array($this, &$url))) {
147 $url = common_local_url('profiletagbyid',
148 array('id' => $this->id));
150 Event::handle('EndProfiletagPermalink', array($this, &$url));
155 * Query notices by users associated with this tag,
156 * but first check the cache before hitting the DB.
158 * @param integer $offset offset
159 * @param integer $limit maximum no of results
160 * @param integer $since_id=null since this id
161 * @param integer $max_id=null maximum id in result
163 * @return Notice the query
166 function getNotices($offset, $limit, $since_id=null, $max_id=null)
168 $ids = Notice::stream(array($this, '_streamDirect'),
170 'profile_tag:notice_ids:' . $this->id,
171 $offset, $limit, $since_id, $max_id);
173 return Notice::getStreamByIds($ids);
177 * Query notices by users associated with this tag from the database.
179 * @param integer $offset offset
180 * @param integer $limit maximum no of results
181 * @param integer $since_id=null since this id
182 * @param integer $max_id=null maximum id in result
184 * @return array array of notice ids.
187 function _streamDirect($offset, $limit, $since_id, $max_id)
189 $inbox = new Profile_tag_inbox();
191 $inbox->profile_tag_id = $this->id;
194 $inbox->selectAdd('notice_id');
196 if ($since_id != 0) {
197 $inbox->whereAdd('notice_id > ' . $since_id);
201 $inbox->whereAdd('notice_id <= ' . $max_id);
204 $inbox->orderBy('notice_id DESC');
206 if (!is_null($offset)) {
207 $inbox->limit($offset, $limit);
212 if ($inbox->find()) {
213 while ($inbox->fetch()) {
214 $ids[] = $inbox->notice_id;
222 * Get subscribers (local and remote) to this people tag
223 * Order by reverse chronology
225 * @param integer $offset offset
226 * @param integer $limit maximum no of results
227 * @param integer $since_id=null since unix timestamp
228 * @param integer $upto=null maximum unix timestamp when subscription was made
230 * @return Profile results
233 function getSubscribers($offset=0, $limit=null, $since=0, $upto=0)
235 $subs = new Profile();
236 $sub = new Profile_tag_subscription();
237 $sub->profile_tag_id = $this->id;
239 $subs->joinAdd($sub);
240 $subs->selectAdd('unix_timestamp(profile_tag_subscription.' .
241 'created) as "cursor"');
244 $subs->whereAdd('cursor > ' . $since);
248 $subs->whereAdd('cursor <= ' . $upto);
251 if ($limit != null) {
252 $subs->limit($offset, $limit);
255 $subs->orderBy('profile_tag_subscription.created DESC');
262 * Get all and only local subscribers to this people tag
263 * used for distributing notices to user inboxes.
265 * @return array ids of users
268 function getUserSubscribers()
273 if(common_config('db','quote_identifiers'))
274 $user_table = '"user"';
275 else $user_table = 'user';
279 'FROM '. $user_table .' JOIN profile_tag_subscription '.
280 'ON '. $user_table .'.id = profile_tag_subscription.profile_id ' .
281 'WHERE profile_tag_subscription.profile_tag_id = %d ';
283 $user->query(sprintf($qry, $this->id));
287 while ($user->fetch()) {
297 * Check to see if a given profile has
298 * subscribed to this people tag's timeline
300 * @param mixed $id User or Profile object or integer id
302 * @return boolean subscription status
305 function hasSubscriber($id)
307 if (!is_numeric($id)) {
311 $sub = Profile_tag_subscription::pkeyGet(array('profile_tag_id' => $this->id,
312 'profile_id' => $id));
317 * Get profiles tagged with this people tag,
318 * include modified timestamp as a "cursor" field
319 * order by descending order of modified time
321 * @param integer $offset offset
322 * @param integer $limit maximum no of results
323 * @param integer $since_id=null since unix timestamp
324 * @param integer $upto=null maximum unix timestamp when subscription was made
326 * @return Profile results
329 function getTagged($offset=0, $limit=null, $since=0, $upto=0)
331 $tagged = new Profile();
332 $tagged->joinAdd(array('id', 'profile_tag:tagged'));
335 $tagged->selectAdd('unix_timestamp(profile_tag.modified) as "cursor"');
336 $tagged->whereAdd('profile_tag.tagger = '.$this->tagger);
337 $tagged->whereAdd("profile_tag.tag = '{$this->tag}'");
340 $tagged->whereAdd('cursor > ' . $since);
344 $tagged->whereAdd('cursor <= ' . $upto);
347 if ($limit != null) {
348 $tagged->limit($offset, $limit);
351 $tagged->orderBy('profile_tag.modified DESC');
358 * Gracefully delete one or many people tags
359 * along with their members and subscriptions data
361 * @return boolean success
366 // force delete one item at a time.
367 if (empty($this->id)) {
369 while ($this->fetch()) {
374 Profile_tag::cleanup($this);
375 Profile_tag_subscription::cleanup($this);
377 return parent::delete();
381 * Update a people tag gracefully
382 * also change "tag" fields in profile_tag table
384 * @param Profile_list $orig Object's original form
386 * @return boolean success
389 function update($orig=null)
393 if (!is_object($orig) && !$orig instanceof Profile_list) {
394 parent::update($orig);
397 // if original tag was different
398 // check to see if the new tag already exists
399 // if not, rename the tag correctly
400 if($orig->tag != $this->tag || $orig->tagger != $this->tagger) {
401 $existing = Profile_list::getByTaggerAndTag($this->tagger, $this->tag);
402 if(!empty($existing)) {
403 throw new ServerException(_('The tag you are trying to rename ' .
404 'to already exists.'));
407 // XXX: allow OStatus plugin to send out profile tag
408 $result = Profile_tag::moveTag($orig, $this);
410 parent::update($orig);
415 * return an xml string representing this people tag
416 * as the author of an atom feed
418 * @return string atom author element
421 function asAtomAuthor()
423 $xs = new XMLStringer(true);
425 $tagger = $this->getTagger();
426 $xs->elementStart('author');
427 $xs->element('name', null, '@' . $tagger->nickname . '/' . $this->tag);
428 $xs->element('uri', null, $this->permalink());
429 $xs->elementEnd('author');
431 return $xs->getString();
435 * return an xml string to represent this people tag
436 * as the subject of an activitystreams feed.
438 * @return string activitystreams subject
441 function asActivitySubject()
443 return $this->asActivityNoun('subject');
447 * return an xml string to represent this people tag
448 * as a noun in an activitystreams feed.
450 * @param string $element the xml tag
452 * @return string activitystreams noun
455 function asActivityNoun($element)
457 $noun = ActivityObject::fromPeopletag($this);
458 return $noun->asString('activity:' . $element);
462 * get the cached number of profiles tagged with this
463 * people tag, re-count if the argument is true.
465 * @param boolean $recount whether to ignore cache
467 * @return integer count
470 function taggedCount($recount=false)
473 return $this->tagged_count;
476 $tags = new Profile_tag();
477 $tags->tag = $this->tag;
478 $tags->tagger = $this->tagger;
479 $orig = clone($this);
480 $this->tagged_count = (int) $tags->count('distinct tagged');
481 $this->update($orig);
483 return $this->tagged_count;
487 * get the cached number of profiles subscribed to this
488 * people tag, re-count if the argument is true.
490 * @param boolean $recount whether to ignore cache
492 * @return integer count
495 function subscriberCount($recount=false)
498 return $this->subscriber_count;
501 $sub = new Profile_tag_subscription();
502 $sub->profile_tag_id = $this->id;
503 $orig = clone($this);
504 $this->subscriber_count = (int) $sub->count('distinct profile_id');
505 $this->update($orig);
507 return $this->subscriber_count;
511 * get the Profile_list object by the
512 * given tagger and with given tag
514 * @param integer $tagger the id of the creator profile
515 * @param integer $tag the tag
517 * @return integer count
520 static function getByTaggerAndTag($tagger, $tag)
522 $ptag = Profile_list::pkeyGet(array('tagger' => $tagger, 'tag' => $tag));
527 * create a profile_list record for a tag, tagger pair
528 * if it doesn't exist, return it.
530 * @param integer $tagger the tagger
531 * @param string $tag the tag
532 * @param string $description description
533 * @param boolean $private protected or not
535 * @return Profile_list the people tag object
538 static function ensureTag($tagger, $tag, $description=null, $private=false)
540 $ptag = Profile_list::getByTaggerAndTag($tagger, $tag);
542 if(empty($ptag->id)) {
546 'description' => $description,
547 'private' => $private
550 $new_tag = Profile_list::saveNew($args);
558 * get the maximum number of characters
559 * that can be used in the description of
562 * determined by $config['peopletag']['desclimit']
563 * if not set, falls back to $config['site']['textlimit']
565 * @return integer maximum number of characters
568 static function maxDescription()
570 $desclimit = common_config('peopletag', 'desclimit');
571 // null => use global limit (distinct from 0!)
572 if (is_null($desclimit)) {
573 $desclimit = common_config('site', 'textlimit');
579 * check if the length of given text exceeds
582 * @param string $desc the description
584 * @return boolean is the descripition too long?
587 static function descriptionTooLong($desc)
589 $desclimit = self::maxDescription();
590 return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
594 * save a new people tag, this should be always used
595 * since it makes uri, homeurl, created and modified
596 * timestamps and performs checks.
598 * @param array $fields an array with fields and their values
600 * @return mixed Profile_list on success, false on fail
602 static function saveNew($fields) {
606 $ptag = new Profile_list();
608 $ptag->query('BEGIN');
610 if (empty($tagger)) {
611 throw new Exception(_('No tagger specified.'));
615 throw new Exception(_('No tag specified.'));
618 if (empty($mainpage)) {
627 if (empty($mainpage)) {
631 if (empty($description)) {
635 if (empty($private)) {
639 $ptag->tagger = $tagger;
641 $ptag->description = $description;
642 $ptag->private = $private;
644 $ptag->mainpage = $mainpage;
645 $ptag->created = common_sql_now();
646 $ptag->modified = common_sql_now();
648 $result = $ptag->insert();
651 common_log_db_error($ptag, 'INSERT', __FILE__);
652 throw new ServerException(_('Could not create profile tag.'));
655 if (!isset($uri) || empty($uri)) {
656 $orig = clone($ptag);
657 $ptag->uri = common_local_url('profiletagbyid', array('id' => $ptag->id, 'tagger_id' => $ptag->tagger));
658 $result = $ptag->update($orig);
660 common_log_db_error($ptag, 'UPDATE', __FILE__);
661 throw new ServerException(_('Could not set profile tag URI.'));
665 if (!isset($mainpage) || empty($mainpage)) {
666 $orig = clone($ptag);
667 $user = User::staticGet('id', $ptag->tagger);
669 $ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'tagger' => $user->nickname));
671 $ptag->mainpage = $uri; // assume this is a remote peopletag and the uri works
674 $result = $ptag->update($orig);
676 common_log_db_error($ptag, 'UPDATE', __FILE__);
677 throw new ServerException(_('Could not set profile tag mainpage.'));
684 * get all items at given cursor position for api
686 * @param callback $fn a function that takes the following arguments in order:
687 * $offset, $limit, $since_id, $max_id
688 * and returns a Profile_list object after making the DB query
689 * @param array $args arguments required for $fn
690 * @param integer $cursor the cursor
691 * @param integer $count max. number of results
694 * - if cursor is 0, return empty list
695 * - if cursor is -1, get first 21 items, next_cursor = 20th prev_cursor = 0
696 * - if cursor is +ve get 22 consecutive items before starting at cursor
697 * - return items[1..20] if items[0] == cursor else return items[0..21]
698 * - prev_cursor = items[1]
699 * - next_cursor = id of the last item being returned
701 * - if cursor is -ve get 22 consecutive items after cursor starting at cursor
702 * - return items[1..20]
704 * @returns array (array (mixed items), int next_cursor, int previous_cursor)
707 // XXX: This should be in Memcached_DataObject... eventually.
709 static function getAtCursor($fn, $args, $cursor, $count=20)
719 // if cursor is +ve fetch $count+2 items before cursor starting at cursor
721 $fn_args = array_merge($args, array(0, $count+2, 0, $max_id));
722 $list = call_user_func_array($fn, $fn_args);
723 while($list->fetch()) {
724 $items[] = clone($list);
727 if ((isset($items[0]->cursor) && $items[0]->cursor == $cursor) ||
728 $items[0]->id == $cursor) {
730 $prev_cursor = isset($items[0]->cursor) ?
731 -$items[0]->cursor : -$items[0]->id;
733 if (count($items) > $count+1) {
736 // this means the cursor item has been deleted, check to see if there are more
737 $fn_args = array_merge($args, array(0, 1, $cursor));
738 $more = call_user_func($fn, $fn_args);
739 if (!$more->fetch() || empty($more)) {
743 $prev_cursor = isset($items[0]->cursor) ?
744 -$items[0]->cursor : -$items[0]->id;
748 if (count($items)==$count+1) {
749 // this means there is a next page.
750 $next = array_pop($items);
751 $next_cursor = isset($next->cursor) ?
752 $items[$count-1]->cursor : $items[$count-1]->id;
755 } else if($cursor < -1) {
756 // if cursor is -ve fetch $count+2 items created after -$cursor-1
757 $cursor = abs($cursor);
758 $since_id = $cursor-1;
760 $fn_args = array_merge($args, array(0, $count+2, $since_id));
761 $list = call_user_func_array($fn, $fn_args);
762 while($list->fetch()) {
763 $items[] = clone($list);
766 $end = count($items)-1;
767 if ((isset($items[$end]->cursor) && $items[$end]->cursor == $cursor) ||
768 $items[$end]->id == $cursor) {
770 $next_cursor = isset($items[$end-1]->cursor) ?
771 $items[$end-1]->cursor : $items[$end-1]->id;
773 $next_cursor = isset($items[$end]->cursor) ?
774 $items[$end]->cursor : $items[$end]->id;
775 if ($end > $count) array_pop($items); // excess item.
777 // check if there are more items for next page
778 $fn_args = array_merge($args, array(0, 1, 0, $cursor));
779 $more = call_user_func_array($fn, $fn_args);
780 if (!$more->fetch() || empty($more)) {
785 if (count($items) == $count+1) {
786 // this means there is a previous page.
787 $prev = array_shift($items);
788 $prev_cursor = isset($prev->cursor) ?
789 -$items[0]->cursor : -$items[0]->id;
791 } else if($cursor == -1) {
792 $fn_args = array_merge($args, array(0, $count+1));
793 $list = call_user_func_array($fn, $fn_args);
795 while($list->fetch()) {
796 $items[] = clone($list);
799 if (count($items)==$count+1) {
800 $next = array_pop($items);
801 if(isset($next->cursor)) {
802 $next_cursor = $items[$count-1]->cursor;
804 $next_cursor = $items[$count-1]->id;
809 return array($items, $next_cursor, $prev_cursor);
813 * save a collection of people tags into the cache
815 * @param string $ckey cache key
816 * @param Profile_list &$tag the results to store
817 * @param integer $offset offset for slicing results
818 * @param integer $limit maximum number of results
820 * @return boolean success
823 static function setCache($ckey, &$tag, $offset=0, $limit=null) {
824 $cache = Cache::instance();
830 while ($tag->fetch()) {
831 $str .= $tag->tagger . ':' . $tag->tag . ';';
832 $tags[] = clone($tag);
834 $str = substr($str, 0, -1);
835 if ($offset>=0 && !is_null($limit)) {
836 $tags = array_slice($tags, $offset, $limit);
839 $tag = new ArrayWrapper($tags);
841 return self::cacheSet($ckey, $str);
845 * get people tags from the cache
847 * @param string $ckey cache key
848 * @param integer $offset offset for slicing
849 * @param integer $limit limit
851 * @return Profile_list results
854 static function getCached($ckey, $offset=0, $limit=null) {
856 $keys_str = self::cacheGet($ckey);
857 if ($keys_str === false) {
861 $pairs = explode(';', $keys_str);
863 foreach ($pairs as $pair) {
864 $keys[] = explode(':', $pair);
867 if ($offset>=0 && !is_null($limit)) {
868 $keys = array_slice($keys, $offset, $limit);
870 return self::getByKeys($keys);
874 * get Profile_list objects from the database
875 * given their (tag, tagger) key pairs.
877 * @param array $keys array of array(tagger, tag)
879 * @return Profile_list results
882 static function getByKeys($keys) {
883 $cache = Cache::instance();
885 if (!empty($cache)) {
888 foreach ($keys as $key) {
889 $t = Profile_list::getByTaggerAndTag($key[0], $key[1]);
894 return new ArrayWrapper($tags);
896 $tag = new Profile_list();
898 //if no IDs requested, just return the tag object
903 foreach ($keys as $key) {
904 $pairs[] = '(' . $key[0] . ', "' . $key[1] . '")';
907 $tag->whereAdd('(tagger, tag) in (' . implode(', ', $pairs) . ')');
913 while ($tag->fetch()) {
914 $temp[$tag->tagger.'-'.$tag->tag] = clone($tag);
919 foreach ($keys as $key) {
920 $id = $key[0].'-'.$key[1];
921 if (array_key_exists($id, $temp)) {
922 $wrapped[] = $temp[$id];
926 return new ArrayWrapper($wrapped);