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 Managed_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 Memcached_DataObject::staticGet('Profile_list',$k,$v); }
54 /* the code above is auto generated do not remove the tag below */
57 public static function schemaDef()
61 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
62 'tagger' => array('type' => 'int', 'not null' => true, 'description' => 'user making the tag'),
63 'tag' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'people tag'),
64 'description' => array('type' => 'text', 'description' => 'description of the people tag'),
65 'private' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'is this tag private'),
67 'created' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date the tag was added'),
68 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date the tag was modified'),
70 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universal identifier'),
71 'mainpage' => array('type' => 'varchar', 'length' => 255, 'description' => 'page to link to'),
72 'tagged_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of people tagged with this tag by this user'),
73 'subscriber_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of subscribers to this tag'),
75 'primary key' => array('tagger', 'tag'),
76 'unique keys' => array(
77 'profile_list_id_key' => array('id')
79 'foreign keys' => array(
80 'profile_list_tagger_fkey' => array('profile', array('tagger' => 'id')),
83 'profile_list_modified_idx' => array('modified'),
84 'profile_list_tag_idx' => array('tag'),
85 'profile_list_tagger_tag_idx' => array('tagger', 'tag'),
86 'profile_list_tagged_count_idx' => array('tagged_count'),
87 'profile_list_subscriber_count_idx' => array('subscriber_count'),
93 * return a profile_list record, given its tag and tagger.
95 * @param array $kv ideally array('tag' => $tag, 'tagger' => $tagger)
97 * @return Profile_list a Profile_list object with the given tag and tagger.
100 function pkeyGet($kv)
102 return Memcached_DataObject::pkeyGet('Profile_list', $kv);
106 * get the tagger of this profile_list object
108 * @return Profile the tagger
113 return Profile::staticGet('id', $this->tagger);
117 * return a string to identify this
118 * profile_list in the user interface etc.
123 function getBestName()
129 * return a uri string for this profile_list
137 if (Event::handle('StartProfiletagGetUri', array($this, &$uri))) {
138 if (!empty($this->uri)) {
141 $uri = common_local_url('profiletagbyid',
142 array('id' => $this->id, 'tagger_id' => $this->tagger));
145 Event::handle('EndProfiletagGetUri', array($this, &$uri));
150 * return a url to the homepage of this item
152 * @return String home url
158 if (Event::handle('StartUserPeopletagHomeUrl', array($this, &$url))) {
159 // normally stored in mainpage, but older ones may be null
160 if (!empty($this->mainpage)) {
161 $url = $this->mainpage;
163 $url = common_local_url('showprofiletag',
164 array('tagger' => $this->getTagger()->nickname,
165 'tag' => $this->tag));
168 Event::handle('EndUserPeopletagHomeUrl', array($this, &$url));
173 * return an immutable url for this object
175 * @return String permalink
181 if (Event::handle('StartProfiletagPermalink', array($this, &$url))) {
182 $url = common_local_url('profiletagbyid',
183 array('id' => $this->id));
185 Event::handle('EndProfiletagPermalink', array($this, &$url));
190 * Query notices by users associated with this tag,
191 * but first check the cache before hitting the DB.
193 * @param integer $offset offset
194 * @param integer $limit maximum no of results
195 * @param integer $since_id=null since this id
196 * @param integer $max_id=null maximum id in result
198 * @return Notice the query
201 function getNotices($offset, $limit, $since_id=null, $max_id=null)
203 $stream = new PeopletagNoticeStream($this);
205 return $stream->getNotices($offset, $limit, $since_id, $max_id);
209 * Get subscribers (local and remote) to this people tag
210 * Order by reverse chronology
212 * @param integer $offset offset
213 * @param integer $limit maximum no of results
214 * @param integer $since_id=null since unix timestamp
215 * @param integer $upto=null maximum unix timestamp when subscription was made
217 * @return Profile results
220 function getSubscribers($offset=0, $limit=null, $since=0, $upto=0)
222 $subs = new Profile();
223 $sub = new Profile_tag_subscription();
224 $sub->profile_tag_id = $this->id;
226 $subs->joinAdd($sub);
227 $subs->selectAdd('unix_timestamp(profile_tag_subscription.' .
228 'created) as "cursor"');
231 $subs->whereAdd('cursor > ' . $since);
235 $subs->whereAdd('cursor <= ' . $upto);
238 if ($limit != null) {
239 $subs->limit($offset, $limit);
242 $subs->orderBy('profile_tag_subscription.created DESC');
249 * Get all and only local subscribers to this people tag
250 * used for distributing notices to user inboxes.
252 * @return array ids of users
255 function getUserSubscribers()
260 if(common_config('db','quote_identifiers'))
261 $user_table = '"user"';
262 else $user_table = 'user';
266 'FROM '. $user_table .' JOIN profile_tag_subscription '.
267 'ON '. $user_table .'.id = profile_tag_subscription.profile_id ' .
268 'WHERE profile_tag_subscription.profile_tag_id = %d ';
270 $user->query(sprintf($qry, $this->id));
274 while ($user->fetch()) {
284 * Check to see if a given profile has
285 * subscribed to this people tag's timeline
287 * @param mixed $id User or Profile object or integer id
289 * @return boolean subscription status
292 function hasSubscriber($id)
294 if (!is_numeric($id)) {
298 $sub = Profile_tag_subscription::pkeyGet(array('profile_tag_id' => $this->id,
299 'profile_id' => $id));
304 * Get profiles tagged with this people tag,
305 * include modified timestamp as a "cursor" field
306 * order by descending order of modified time
308 * @param integer $offset offset
309 * @param integer $limit maximum no of results
310 * @param integer $since_id=null since unix timestamp
311 * @param integer $upto=null maximum unix timestamp when subscription was made
313 * @return Profile results
316 function getTagged($offset=0, $limit=null, $since=0, $upto=0)
318 $tagged = new Profile();
319 $tagged->joinAdd(array('id', 'profile_tag:tagged'));
322 $tagged->selectAdd('unix_timestamp(profile_tag.modified) as "cursor"');
323 $tagged->whereAdd('profile_tag.tagger = '.$this->tagger);
324 $tagged->whereAdd("profile_tag.tag = '{$this->tag}'");
327 $tagged->whereAdd('cursor > ' . $since);
331 $tagged->whereAdd('cursor <= ' . $upto);
334 if ($limit != null) {
335 $tagged->limit($offset, $limit);
338 $tagged->orderBy('profile_tag.modified DESC');
345 * Gracefully delete one or many people tags
346 * along with their members and subscriptions data
348 * @return boolean success
353 // force delete one item at a time.
354 if (empty($this->id)) {
356 while ($this->fetch()) {
361 Profile_tag::cleanup($this);
362 Profile_tag_subscription::cleanup($this);
364 self::blow('profile:lists:%d', $this->tagger);
366 return parent::delete();
370 * Update a people tag gracefully
371 * also change "tag" fields in profile_tag table
373 * @param Profile_list $orig Object's original form
375 * @return boolean success
378 function update($orig=null)
382 if (!is_object($orig) && !$orig instanceof Profile_list) {
383 parent::update($orig);
386 // if original tag was different
387 // check to see if the new tag already exists
388 // if not, rename the tag correctly
389 if($orig->tag != $this->tag || $orig->tagger != $this->tagger) {
390 $existing = Profile_list::getByTaggerAndTag($this->tagger, $this->tag);
391 if(!empty($existing)) {
392 // TRANS: Server exception.
393 throw new ServerException(_('The tag you are trying to rename ' .
394 'to already exists.'));
397 // XXX: allow OStatus plugin to send out profile tag
398 $result = Profile_tag::moveTag($orig, $this);
400 parent::update($orig);
405 * return an xml string representing this people tag
406 * as the author of an atom feed
408 * @return string atom author element
411 function asAtomAuthor()
413 $xs = new XMLStringer(true);
415 $tagger = $this->getTagger();
416 $xs->elementStart('author');
417 $xs->element('name', null, '@' . $tagger->nickname . '/' . $this->tag);
418 $xs->element('uri', null, $this->permalink());
419 $xs->elementEnd('author');
421 return $xs->getString();
425 * return an xml string to represent this people tag
426 * as the subject of an activitystreams feed.
428 * @return string activitystreams subject
431 function asActivitySubject()
433 return $this->asActivityNoun('subject');
437 * return an xml string to represent this people tag
438 * as a noun in an activitystreams feed.
440 * @param string $element the xml tag
442 * @return string activitystreams noun
445 function asActivityNoun($element)
447 $noun = ActivityObject::fromPeopletag($this);
448 return $noun->asString('activity:' . $element);
452 * get the cached number of profiles tagged with this
453 * people tag, re-count if the argument is true.
455 * @param boolean $recount whether to ignore cache
457 * @return integer count
460 function taggedCount($recount=false)
462 $keypart = sprintf('profile_list:tagged_count:%d:%s',
466 $count = self::cacheGet($keypart);
468 if ($count === false) {
469 $tags = new Profile_tag();
471 $tags->tag = $this->tag;
472 $tags->tagger = $this->tagger;
474 $count = $tags->count('distinct tagged');
476 self::cacheSet($keypart, $count);
483 * get the cached number of profiles subscribed to this
484 * people tag, re-count if the argument is true.
486 * @param boolean $recount whether to ignore cache
488 * @return integer count
491 function subscriberCount($recount=false)
493 $keypart = sprintf('profile_list:subscriber_count:%d',
496 $count = self::cacheGet($keypart);
498 if ($count === false) {
500 $sub = new Profile_tag_subscription();
501 $sub->profile_tag_id = $this->id;
502 $count = (int) $sub->count('distinct profile_id');
504 self::cacheSet($keypart, $count);
511 * get the cached number of profiles subscribed to this
512 * people tag, re-count if the argument is true.
514 * @param boolean $recount whether to ignore cache
516 * @return integer count
519 function blowNoticeStreamCache($all=false)
521 self::blow('profile_list:notice_ids:%d', $this->id);
523 self::blow('profile_list:notice_ids:%d;last', $this->id);
528 * get the Profile_list object by the
529 * given tagger and with given tag
531 * @param integer $tagger the id of the creator profile
532 * @param integer $tag the tag
534 * @return integer count
537 static function getByTaggerAndTag($tagger, $tag)
539 $ptag = Profile_list::pkeyGet(array('tagger' => $tagger, 'tag' => $tag));
544 * create a profile_list record for a tag, tagger pair
545 * if it doesn't exist, return it.
547 * @param integer $tagger the tagger
548 * @param string $tag the tag
549 * @param string $description description
550 * @param boolean $private protected or not
552 * @return Profile_list the people tag object
555 static function ensureTag($tagger, $tag, $description=null, $private=false)
557 $ptag = Profile_list::getByTaggerAndTag($tagger, $tag);
559 if(empty($ptag->id)) {
563 'description' => $description,
564 'private' => $private
567 $new_tag = Profile_list::saveNew($args);
575 * get the maximum number of characters
576 * that can be used in the description of
579 * determined by $config['peopletag']['desclimit']
580 * if not set, falls back to $config['site']['textlimit']
582 * @return integer maximum number of characters
585 static function maxDescription()
587 $desclimit = common_config('peopletag', 'desclimit');
588 // null => use global limit (distinct from 0!)
589 if (is_null($desclimit)) {
590 $desclimit = common_config('site', 'textlimit');
596 * check if the length of given text exceeds
599 * @param string $desc the description
601 * @return boolean is the descripition too long?
604 static function descriptionTooLong($desc)
606 $desclimit = self::maxDescription();
607 return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
611 * save a new people tag, this should be always used
612 * since it makes uri, homeurl, created and modified
613 * timestamps and performs checks.
615 * @param array $fields an array with fields and their values
617 * @return mixed Profile_list on success, false on fail
619 static function saveNew($fields) {
622 $ptag = new Profile_list();
624 $ptag->query('BEGIN');
626 if (empty($tagger)) {
627 // TRANS: Server exception saving new tag without having a tagger specified.
628 throw new Exception(_('No tagger specified.'));
632 // TRANS: Server exception saving new tag without having a tag specified.
633 throw new Exception(_('No tag specified.'));
636 if (empty($mainpage)) {
645 if (empty($mainpage)) {
649 if (empty($description)) {
653 if (empty($private)) {
657 $ptag->tagger = $tagger;
659 $ptag->description = $description;
660 $ptag->private = $private;
662 $ptag->mainpage = $mainpage;
663 $ptag->created = common_sql_now();
664 $ptag->modified = common_sql_now();
666 $result = $ptag->insert();
669 common_log_db_error($ptag, 'INSERT', __FILE__);
670 // TRANS: Server exception saving new tag.
671 throw new ServerException(_('Could not create profile tag.'));
674 if (!isset($uri) || empty($uri)) {
675 $orig = clone($ptag);
676 $ptag->uri = common_local_url('profiletagbyid', array('id' => $ptag->id, 'tagger_id' => $ptag->tagger));
677 $result = $ptag->update($orig);
679 common_log_db_error($ptag, 'UPDATE', __FILE__);
680 // TRANS: Server exception saving new tag.
681 throw new ServerException(_('Could not set profile tag URI.'));
685 if (!isset($mainpage) || empty($mainpage)) {
686 $orig = clone($ptag);
687 $user = User::staticGet('id', $ptag->tagger);
689 $ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'tagger' => $user->nickname));
691 $ptag->mainpage = $uri; // assume this is a remote peopletag and the uri works
694 $result = $ptag->update($orig);
696 common_log_db_error($ptag, 'UPDATE', __FILE__);
697 // TRANS: Server exception saving new tag.
698 throw new ServerException(_('Could not set profile tag mainpage.'));
705 * get all items at given cursor position for api
707 * @param callback $fn a function that takes the following arguments in order:
708 * $offset, $limit, $since_id, $max_id
709 * and returns a Profile_list object after making the DB query
710 * @param array $args arguments required for $fn
711 * @param integer $cursor the cursor
712 * @param integer $count max. number of results
715 * - if cursor is 0, return empty list
716 * - if cursor is -1, get first 21 items, next_cursor = 20th prev_cursor = 0
717 * - if cursor is +ve get 22 consecutive items before starting at cursor
718 * - return items[1..20] if items[0] == cursor else return items[0..21]
719 * - prev_cursor = items[1]
720 * - next_cursor = id of the last item being returned
722 * - if cursor is -ve get 22 consecutive items after cursor starting at cursor
723 * - return items[1..20]
725 * @returns array (array (mixed items), int next_cursor, int previous_cursor)
728 // XXX: This should be in Memcached_DataObject... eventually.
730 static function getAtCursor($fn, $args, $cursor, $count=20)
740 // if cursor is +ve fetch $count+2 items before cursor starting at cursor
742 $fn_args = array_merge($args, array(0, $count+2, 0, $max_id));
743 $list = call_user_func_array($fn, $fn_args);
744 while($list->fetch()) {
745 $items[] = clone($list);
748 if ((isset($items[0]->cursor) && $items[0]->cursor == $cursor) ||
749 $items[0]->id == $cursor) {
751 $prev_cursor = isset($items[0]->cursor) ?
752 -$items[0]->cursor : -$items[0]->id;
754 if (count($items) > $count+1) {
757 // this means the cursor item has been deleted, check to see if there are more
758 $fn_args = array_merge($args, array(0, 1, $cursor));
759 $more = call_user_func($fn, $fn_args);
760 if (!$more->fetch() || empty($more)) {
764 $prev_cursor = isset($items[0]->cursor) ?
765 -$items[0]->cursor : -$items[0]->id;
769 if (count($items)==$count+1) {
770 // this means there is a next page.
771 $next = array_pop($items);
772 $next_cursor = isset($next->cursor) ?
773 $items[$count-1]->cursor : $items[$count-1]->id;
776 } else if($cursor < -1) {
777 // if cursor is -ve fetch $count+2 items created after -$cursor-1
778 $cursor = abs($cursor);
779 $since_id = $cursor-1;
781 $fn_args = array_merge($args, array(0, $count+2, $since_id));
782 $list = call_user_func_array($fn, $fn_args);
783 while($list->fetch()) {
784 $items[] = clone($list);
787 $end = count($items)-1;
788 if ((isset($items[$end]->cursor) && $items[$end]->cursor == $cursor) ||
789 $items[$end]->id == $cursor) {
791 $next_cursor = isset($items[$end-1]->cursor) ?
792 $items[$end-1]->cursor : $items[$end-1]->id;
794 $next_cursor = isset($items[$end]->cursor) ?
795 $items[$end]->cursor : $items[$end]->id;
796 if ($end > $count) array_pop($items); // excess item.
798 // check if there are more items for next page
799 $fn_args = array_merge($args, array(0, 1, 0, $cursor));
800 $more = call_user_func_array($fn, $fn_args);
801 if (!$more->fetch() || empty($more)) {
806 if (count($items) == $count+1) {
807 // this means there is a previous page.
808 $prev = array_shift($items);
809 $prev_cursor = isset($prev->cursor) ?
810 -$items[0]->cursor : -$items[0]->id;
812 } else if($cursor == -1) {
813 $fn_args = array_merge($args, array(0, $count+1));
814 $list = call_user_func_array($fn, $fn_args);
816 while($list->fetch()) {
817 $items[] = clone($list);
820 if (count($items)==$count+1) {
821 $next = array_pop($items);
822 if(isset($next->cursor)) {
823 $next_cursor = $items[$count-1]->cursor;
825 $next_cursor = $items[$count-1]->id;
830 return array($items, $next_cursor, $prev_cursor);
834 * save a collection of people tags into the cache
836 * @param string $ckey cache key
837 * @param Profile_list &$tag the results to store
838 * @param integer $offset offset for slicing results
839 * @param integer $limit maximum number of results
841 * @return boolean success
844 static function setCache($ckey, &$tag, $offset=0, $limit=null) {
845 $cache = Cache::instance();
851 while ($tag->fetch()) {
852 $str .= $tag->tagger . ':' . $tag->tag . ';';
853 $tags[] = clone($tag);
855 $str = substr($str, 0, -1);
856 if ($offset>=0 && !is_null($limit)) {
857 $tags = array_slice($tags, $offset, $limit);
860 $tag = new ArrayWrapper($tags);
862 return self::cacheSet($ckey, $str);
866 * get people tags from the cache
868 * @param string $ckey cache key
869 * @param integer $offset offset for slicing
870 * @param integer $limit limit
872 * @return Profile_list results
875 static function getCached($ckey, $offset=0, $limit=null) {
877 $keys_str = self::cacheGet($ckey);
878 if ($keys_str === false) {
882 $pairs = explode(';', $keys_str);
884 foreach ($pairs as $pair) {
885 $keys[] = explode(':', $pair);
888 if ($offset>=0 && !is_null($limit)) {
889 $keys = array_slice($keys, $offset, $limit);
891 return self::getByKeys($keys);
895 * get Profile_list objects from the database
896 * given their (tag, tagger) key pairs.
898 * @param array $keys array of array(tagger, tag)
900 * @return Profile_list results
903 static function getByKeys($keys) {
904 $cache = Cache::instance();
906 if (!empty($cache)) {
909 foreach ($keys as $key) {
910 $t = Profile_list::getByTaggerAndTag($key[0], $key[1]);
915 return new ArrayWrapper($tags);
917 $tag = new Profile_list();
919 //if no IDs requested, just return the tag object
924 foreach ($keys as $key) {
925 $pairs[] = '(' . $key[0] . ', "' . $key[1] . '")';
928 $tag->whereAdd('(tagger, tag) in (' . implode(', ', $pairs) . ')');
934 while ($tag->fetch()) {
935 $temp[$tag->tagger.'-'.$tag->tag] = clone($tag);
940 foreach ($keys as $key) {
941 $id = $key[0].'-'.$key[1];
942 if (array_key_exists($id, $temp)) {
943 $wrapped[] = $temp[$id];
947 return new ArrayWrapper($wrapped);
953 $result = parent::insert();
955 self::blow('profile:lists:%d', $this->tagger);