]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - classes/Profile_list.php
Merge remote-tracking branch 'upstream/master' into social-master
[quix0rs-gnu-social.git] / classes / Profile_list.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  *
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.
9  *
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.
14  *
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/>.
17  *
18  * @category Notices
19  * @package  StatusNet
20  * @author   Shashi Gowda <connect2shashi@gmail.com>
21  * @license  GNU Affero General Public License http://www.gnu.org/licenses/
22  */
23
24 if (!defined('STATUSNET') && !defined('LACONICA')) {
25     exit(1);
26 }
27
28 /**
29  * Table Definition for profile_list
30  */
31 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
32
33 class Profile_list extends Managed_DataObject
34 {
35     ###START_AUTOCODE
36     /* the code below is auto generated do not remove the above tag */
37
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(191)  unique_key   not 255 because utf8mb4 takes more space
47     public $mainpage;                        // varchar(191)   not 255 because utf8mb4 takes more space
48     public $tagged_count;                    // smallint
49     public $subscriber_count;                // smallint
50
51     /* the code above is auto generated do not remove the tag below */
52     ###END_AUTOCODE
53
54     public static function schemaDef()
55     {
56         return array(
57             'fields' => array(
58                 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
59                 'tagger' => array('type' => 'int', 'not null' => true, 'description' => 'user making the tag'),
60                 'tag' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'people tag'),
61                 'description' => array('type' => 'text', 'description' => 'description of the people tag'),
62                 'private' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'is this tag private'),
63
64                 'created' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date the tag was added'),
65                 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date the tag was modified'),
66
67                 'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universal identifier'),
68                 'mainpage' => array('type' => 'varchar', 'length' => 191, 'description' => 'page to link to'),
69                 'tagged_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of people tagged with this tag by this user'),
70                 'subscriber_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of subscribers to this tag'),
71             ),
72             'primary key' => array('tagger', 'tag'),
73             'unique keys' => array(
74                 'profile_list_id_key' => array('id')
75             ),
76             'foreign keys' => array(
77                 'profile_list_tagger_fkey' => array('profile', array('tagger' => 'id')),
78             ),
79             'indexes' => array(
80                 'profile_list_modified_idx' => array('modified'),
81                 'profile_list_tag_idx' => array('tag'),
82                 'profile_list_tagger_tag_idx' => array('tagger', 'tag'),
83                 'profile_list_tagged_count_idx' => array('tagged_count'),
84                 'profile_list_subscriber_count_idx' => array('subscriber_count'),
85             ),
86         );
87     }
88
89     /**
90      * get the tagger of this profile_list object
91      *
92      * @return Profile the tagger
93      */
94
95     function getTagger()
96     {
97         return Profile::getKV('id', $this->tagger);
98     }
99
100     /**
101      * return a string to identify this
102      * profile_list in the user interface etc.
103      *
104      * @return String
105      */
106
107     function getBestName()
108     {
109         return $this->tag;
110     }
111
112     /**
113      * return a uri string for this profile_list
114      *
115      * @return String uri
116      */
117
118     function getUri()
119     {
120         $uri = null;
121         if (Event::handle('StartProfiletagGetUri', array($this, &$uri))) {
122             if (!empty($this->uri)) {
123                 $uri = $this->uri;
124             } else {
125                 $uri = common_local_url('profiletagbyid',
126                                         array('id' => $this->id, 'tagger_id' => $this->tagger));
127             }
128         }
129         Event::handle('EndProfiletagGetUri', array($this, &$uri));
130         return $uri;
131     }
132
133     /**
134      * return a url to the homepage of this item
135      *
136      * @return String home url
137      */
138
139     function homeUrl()
140     {
141         $url = null;
142         if (Event::handle('StartUserPeopletagHomeUrl', array($this, &$url))) {
143             // normally stored in mainpage, but older ones may be null
144             if (!empty($this->mainpage)) {
145                 $url = $this->mainpage;
146             } else {
147                 $url = common_local_url('showprofiletag',
148                                         array('tagger' => $this->getTagger()->nickname,
149                                               'tag'    => $this->tag));
150             }
151         }
152         Event::handle('EndUserPeopletagHomeUrl', array($this, &$url));
153         return $url;
154     }
155
156     /**
157      * return an immutable url for this object
158      *
159      * @return String permalink
160      */
161
162     function permalink()
163     {
164         $url = null;
165         if (Event::handle('StartProfiletagPermalink', array($this, &$url))) {
166             $url = common_local_url('profiletagbyid',
167                                     array('id' => $this->id));
168         }
169         Event::handle('EndProfiletagPermalink', array($this, &$url));
170         return $url;
171     }
172
173     /**
174      * Query notices by users associated with this tag,
175      * but first check the cache before hitting the DB.
176      *
177      * @param integer $offset   offset
178      * @param integer $limit    maximum no of results
179      * @param integer $since_id=null    since this id
180      * @param integer $max_id=null  maximum id in result
181      *
182      * @return Notice the query
183      */
184
185     function getNotices($offset, $limit, $since_id=null, $max_id=null)
186     {
187         $stream = new PeopletagNoticeStream($this);
188
189         return $stream->getNotices($offset, $limit, $since_id, $max_id);
190     }
191
192     /**
193      * Get subscribers (local and remote) to this people tag
194      * Order by reverse chronology
195      *
196      * @param integer $offset   offset
197      * @param integer $limit    maximum no of results
198      * @param integer $since_id=null    since unix timestamp
199      * @param integer $upto=null  maximum unix timestamp when subscription was made
200      *
201      * @return Profile results
202      */
203
204     function getSubscribers($offset=0, $limit=null, $since=0, $upto=0)
205     {
206         $subs = new Profile();
207
208         $subs->joinAdd(
209             array('id', 'profile_tag_subscription:profile_id')
210         );
211         $subs->whereAdd('profile_tag_subscription.profile_tag_id = ' . $this->id);
212
213         $subs->selectAdd('unix_timestamp(profile_tag_subscription.' .
214                          'created) as "cursor"');
215
216         if ($since != 0) {
217             $subs->whereAdd('cursor > ' . $since);
218         }
219
220         if ($upto != 0) {
221             $subs->whereAdd('cursor <= ' . $upto);
222         }
223
224         if ($limit != null) {
225             $subs->limit($offset, $limit);
226         }
227
228         $subs->orderBy('profile_tag_subscription.created DESC');
229         $subs->find();
230
231         return $subs;
232     }
233
234     /**
235      * Get all and only local subscribers to this people tag
236      * used for distributing notices to user inboxes.
237      *
238      * @return array ids of users
239      */
240
241     function getUserSubscribers()
242     {
243         // XXX: cache this
244
245         $user = new User();
246         if(common_config('db','quote_identifiers'))
247             $user_table = '"user"';
248         else $user_table = 'user';
249
250         $qry =
251           'SELECT id ' .
252           'FROM '. $user_table .' JOIN profile_tag_subscription '.
253           'ON '. $user_table .'.id = profile_tag_subscription.profile_id ' .
254           'WHERE profile_tag_subscription.profile_tag_id = %d ';
255
256         $user->query(sprintf($qry, $this->id));
257
258         $ids = array();
259
260         while ($user->fetch()) {
261             $ids[] = $user->id;
262         }
263
264         $user->free();
265
266         return $ids;
267     }
268
269     /**
270      * Check to see if a given profile has
271      * subscribed to this people tag's timeline
272      *
273      * @param mixed $id User or Profile object or integer id
274      *
275      * @return boolean subscription status
276      */
277
278     function hasSubscriber($id)
279     {
280         if (!is_numeric($id)) {
281             $id = $id->id;
282         }
283
284         $sub = Profile_tag_subscription::pkeyGet(array('profile_tag_id' => $this->id,
285                                                        'profile_id'     => $id));
286         return !empty($sub);
287     }
288
289     /**
290      * Get profiles tagged with this people tag,
291      * include modified timestamp as a "cursor" field
292      * order by descending order of modified time
293      *
294      * @param integer $offset   offset
295      * @param integer $limit    maximum no of results
296      * @param integer $since_id=null    since unix timestamp
297      * @param integer $upto=null  maximum unix timestamp when subscription was made
298      *
299      * @return Profile results
300      */
301
302     function getTagged($offset=0, $limit=null, $since=0, $upto=0)
303     {
304         $tagged = new Profile();
305         $tagged->joinAdd(array('id', 'profile_tag:tagged'));
306
307         #@fixme: postgres
308         $tagged->selectAdd('unix_timestamp(profile_tag.modified) as "cursor"');
309         $tagged->whereAdd('profile_tag.tagger = '.$this->tagger);
310         $tagged->whereAdd("profile_tag.tag = '{$this->tag}'");
311
312         if ($since != 0) {
313             $tagged->whereAdd('cursor > ' . $since);
314         }
315
316         if ($upto != 0) {
317             $tagged->whereAdd('cursor <= ' . $upto);
318         }
319
320         if ($limit != null) {
321             $tagged->limit($offset, $limit);
322         }
323
324         $tagged->orderBy('profile_tag.modified DESC');
325         $tagged->find();
326
327         return $tagged;
328     }
329
330     /**
331      * Gracefully delete one or many people tags
332      * along with their members and subscriptions data
333      *
334      * @return boolean success
335      */
336
337     function delete($useWhere=false)
338     {
339         // force delete one item at a time.
340         if (empty($this->id)) {
341             $this->find();
342             while ($this->fetch()) {
343                 $this->delete();
344             }
345         }
346
347         Profile_tag::cleanup($this);
348         Profile_tag_subscription::cleanup($this);
349
350         self::blow('profile:lists:%d', $this->tagger);
351
352         return parent::delete($useWhere);
353     }
354
355     /**
356      * Update a people tag gracefully
357      * also change "tag" fields in profile_tag table
358      *
359      * @param Profile_list $dataObject    Object's original form
360      *
361      * @return boolean success
362      */
363
364     function update($dataObject=false)
365     {
366         if (!is_object($dataObject) && !$dataObject instanceof Profile_list) {
367             return parent::update($dataObject);
368         }
369
370         $result = true;
371
372         // if original tag was different
373         // check to see if the new tag already exists
374         // if not, rename the tag correctly
375         if($dataObject->tag != $this->tag || $dataObject->tagger != $this->tagger) {
376             $existing = Profile_list::getByTaggerAndTag($this->tagger, $this->tag);
377             if(!empty($existing)) {
378                 // TRANS: Server exception.
379                 throw new ServerException(_('The tag you are trying to rename ' .
380                                             'to already exists.'));
381             }
382             // move the tag
383             // XXX: allow OStatus plugin to send out profile tag
384             $result = Profile_tag::moveTag($dataObject, $this);
385         }
386         return parent::update($dataObject);
387     }
388
389     /**
390      * return an xml string representing this people tag
391      * as the author of an atom feed
392      *
393      * @return string atom author element
394      */
395
396     function asAtomAuthor()
397     {
398         $xs = new XMLStringer(true);
399
400         $tagger = $this->getTagger();
401         $xs->elementStart('author');
402         $xs->element('name', null, '@' . $tagger->nickname . '/' . $this->tag);
403         $xs->element('uri', null, $this->permalink());
404         $xs->elementEnd('author');
405
406         return $xs->getString();
407     }
408
409     /**
410      * return an xml string to represent this people tag
411      * as a noun in an activitystreams feed.
412      *
413      * @param string $element the xml tag
414      *
415      * @return string activitystreams noun
416      */
417
418     function asActivityNoun($element)
419     {
420         $noun = ActivityObject::fromPeopletag($this);
421         return $noun->asString('activity:' . $element);
422     }
423
424     /**
425      * get the cached number of profiles tagged with this
426      * people tag, re-count if the argument is true.
427      *
428      * @param boolean $recount  whether to ignore cache
429      *
430      * @return integer count
431      */
432
433     function taggedCount($recount=false)
434     {
435         $keypart = sprintf('profile_list:tagged_count:%d:%s', 
436                            $this->tagger,
437                            $this->tag);
438
439         $count = self::cacheGet($keypart);
440
441         if ($count === false) {
442             $tags = new Profile_tag();
443
444             $tags->tag = $this->tag;
445             $tags->tagger = $this->tagger;
446
447             $count = $tags->count('distinct tagged');
448
449             self::cacheSet($keypart, $count);
450         }
451
452         return $count;
453     }
454
455     /**
456      * get the cached number of profiles subscribed to this
457      * people tag, re-count if the argument is true.
458      *
459      * @param boolean $recount  whether to ignore cache
460      *
461      * @return integer count
462      */
463
464     function subscriberCount($recount=false)
465     {
466         $keypart = sprintf('profile_list:subscriber_count:%d', 
467                            $this->id);
468
469         $count = self::cacheGet($keypart);
470
471         if ($count === false) {
472
473             $sub = new Profile_tag_subscription();
474             $sub->profile_tag_id = $this->id;
475             $count = (int) $sub->count('distinct profile_id');
476
477             self::cacheSet($keypart, $count);
478         }
479
480         return $count;
481     }
482
483     /**
484      * get the cached number of profiles subscribed to this
485      * people tag, re-count if the argument is true.
486      *
487      * @param boolean $recount  whether to ignore cache
488      *
489      * @return integer count
490      */
491
492     function blowNoticeStreamCache($all=false)
493     {
494         self::blow('profile_list:notice_ids:%d', $this->id);
495         if ($all) {
496             self::blow('profile_list:notice_ids:%d;last', $this->id);
497         }
498     }
499
500     /**
501      * get the Profile_list object by the
502      * given tagger and with given tag
503      *
504      * @param integer $tagger   the id of the creator profile
505      * @param integer $tag      the tag
506      *
507      * @return integer count
508      */
509
510     static function getByTaggerAndTag($tagger, $tag)
511     {
512         $ptag = Profile_list::pkeyGet(array('tagger' => $tagger, 'tag' => $tag));
513         return $ptag;
514     }
515
516     /**
517      * create a profile_list record for a tag, tagger pair
518      * if it doesn't exist, return it.
519      *
520      * @param integer $tagger   the tagger
521      * @param string  $tag      the tag
522      * @param string  $description description
523      * @param boolean $private  protected or not
524      *
525      * @return Profile_list the people tag object
526      */
527
528     static function ensureTag($tagger, $tag, $description=null, $private=false)
529     {
530         $ptag = Profile_list::getByTaggerAndTag($tagger, $tag);
531
532         if(empty($ptag->id)) {
533             $args = array(
534                 'tag' => $tag,
535                 'tagger' => $tagger,
536                 'description' => $description,
537                 'private' => $private
538             );
539
540             $new_tag = Profile_list::saveNew($args);
541
542             return $new_tag;
543         }
544         return $ptag;
545     }
546
547     /**
548      * get the maximum number of characters
549      * that can be used in the description of
550      * a people tag.
551      *
552      * determined by $config['peopletag']['desclimit']
553      * if not set, falls back to $config['site']['textlimit']
554      *
555      * @return integer maximum number of characters
556      */
557
558     static function maxDescription()
559     {
560         $desclimit = common_config('peopletag', 'desclimit');
561         // null => use global limit (distinct from 0!)
562         if (is_null($desclimit)) {
563             $desclimit = common_config('site', 'textlimit');
564         }
565         return $desclimit;
566     }
567
568     /**
569      * check if the length of given text exceeds
570      * character limit.
571      *
572      * @param string $desc  the description
573      *
574      * @return boolean is the descripition too long?
575      */
576
577     static function descriptionTooLong($desc)
578     {
579         $desclimit = self::maxDescription();
580         return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
581     }
582
583     /**
584      * save a new people tag, this should be always used
585      * since it makes uri, homeurl, created and modified
586      * timestamps and performs checks.
587      *
588      * @param array $fields an array with fields and their values
589      *
590      * @return mixed Profile_list on success, false on fail
591      */
592     static function saveNew(array $fields) {
593         extract($fields);
594
595         $ptag = new Profile_list();
596
597         $ptag->query('BEGIN');
598
599         if (empty($tagger)) {
600             // TRANS: Server exception saving new tag without having a tagger specified.
601             throw new Exception(_('No tagger specified.'));
602         }
603
604         if (empty($tag)) {
605             // TRANS: Server exception saving new tag without having a tag specified.
606             throw new Exception(_('No tag specified.'));
607         }
608
609         if (empty($mainpage)) {
610             $mainpage = null;
611         }
612
613         if (empty($uri)) {
614             // fill in later...
615             $uri = null;
616         }
617
618         if (empty($mainpage)) {
619             $mainpage = null;
620         }
621
622         if (empty($description)) {
623             $description = null;
624         }
625
626         if (empty($private)) {
627             $private = false;
628         }
629
630         $ptag->tagger      = $tagger;
631         $ptag->tag         = $tag;
632         $ptag->description = $description;
633         $ptag->private     = $private;
634         $ptag->uri         = $uri;
635         $ptag->mainpage    = $mainpage;
636         $ptag->created     = common_sql_now();
637         $ptag->modified    = common_sql_now();
638
639         $result = $ptag->insert();
640
641         if (!$result) {
642             common_log_db_error($ptag, 'INSERT', __FILE__);
643             // TRANS: Server exception saving new tag.
644             throw new ServerException(_('Could not create profile tag.'));
645         }
646
647         if (!isset($uri) || empty($uri)) {
648             $orig = clone($ptag);
649             $ptag->uri = common_local_url('profiletagbyid', array('id' => $ptag->id, 'tagger_id' => $ptag->tagger));
650             $result = $ptag->update($orig);
651             if (!$result) {
652                 common_log_db_error($ptag, 'UPDATE', __FILE__);
653             // TRANS: Server exception saving new tag.
654                 throw new ServerException(_('Could not set profile tag URI.'));
655             }
656         }
657
658         if (!isset($mainpage) || empty($mainpage)) {
659             $orig = clone($ptag);
660             $user = User::getKV('id', $ptag->tagger);
661             if(!empty($user)) {
662                 $ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'tagger' => $user->nickname));
663             } else {
664                 $ptag->mainpage = $uri; // assume this is a remote peopletag and the uri works
665             }
666
667             $result = $ptag->update($orig);
668             if (!$result) {
669                 common_log_db_error($ptag, 'UPDATE', __FILE__);
670                 // TRANS: Server exception saving new tag.
671                 throw new ServerException(_('Could not set profile tag mainpage.'));
672             }
673         }
674         return $ptag;
675     }
676
677     /**
678      * get all items at given cursor position for api
679      *
680      * @param callback $fn  a function that takes the following arguments in order:
681      *                      $offset, $limit, $since_id, $max_id
682      *                      and returns a Profile_list object after making the DB query
683      * @param array $args   arguments required for $fn
684      * @param integer $cursor   the cursor
685      * @param integer $count    max. number of results
686      *
687      * Algorithm:
688      * - if cursor is 0, return empty list
689      * - if cursor is -1, get first 21 items, next_cursor = 20th prev_cursor = 0
690      * - if cursor is +ve get 22 consecutive items before starting at cursor
691      *   - return items[1..20] if items[0] == cursor else return items[0..21]
692      *   - prev_cursor = items[1]
693      *   - next_cursor = id of the last item being returned
694      *
695      * - if cursor is -ve get 22 consecutive items after cursor starting at cursor
696      *   - return items[1..20]
697      *
698      * @returns array (array (mixed items), int next_cursor, int previous_cursor)
699      */
700
701      // XXX: This should be in Memcached_DataObject... eventually.
702
703     static function getAtCursor($fn, array $args, $cursor, $count=20)
704     {
705         $items = array();
706
707         $since_id = 0;
708         $max_id = 0;
709         $next_cursor = 0;
710         $prev_cursor = 0;
711
712         if($cursor > 0) {
713             // if cursor is +ve fetch $count+2 items before cursor starting at cursor
714             $max_id = $cursor;
715             $fn_args = array_merge($args, array(0, $count+2, 0, $max_id));
716             $list = call_user_func_array($fn, $fn_args);
717             while($list->fetch()) {
718                 $items[] = clone($list);
719             }
720
721             if ((isset($items[0]->cursor) && $items[0]->cursor == $cursor) ||
722                 $items[0]->id == $cursor) {
723                 array_shift($items);
724                 $prev_cursor = isset($items[0]->cursor) ?
725                     -$items[0]->cursor : -$items[0]->id;
726             } else {
727                 if (count($items) > $count+1) {
728                     array_shift($items);
729                 }
730                 // this means the cursor item has been deleted, check to see if there are more
731                 $fn_args = array_merge($args, array(0, 1, $cursor));
732                 $more = call_user_func($fn, $fn_args);
733                 if (!$more->fetch() || empty($more)) {
734                     // no more items.
735                     $prev_cursor = 0;
736                 } else {
737                     $prev_cursor = isset($items[0]->cursor) ?
738                         -$items[0]->cursor : -$items[0]->id;
739                 }
740             }
741
742             if (count($items)==$count+1) {
743                 // this means there is a next page.
744                 $next = array_pop($items);
745                 $next_cursor = isset($next->cursor) ?
746                     $items[$count-1]->cursor : $items[$count-1]->id;
747             }
748
749         } else if($cursor < -1) {
750             // if cursor is -ve fetch $count+2 items created after -$cursor-1
751             $cursor = abs($cursor);
752             $since_id = $cursor-1;
753
754             $fn_args = array_merge($args, array(0, $count+2, $since_id));
755             $list = call_user_func_array($fn, $fn_args);
756             while($list->fetch()) {
757                 $items[] = clone($list);
758             }
759
760             $end = count($items)-1;
761             if ((isset($items[$end]->cursor) && $items[$end]->cursor == $cursor) ||
762                 $items[$end]->id == $cursor) {
763                 array_pop($items);
764                 $next_cursor = isset($items[$end-1]->cursor) ?
765                     $items[$end-1]->cursor : $items[$end-1]->id;
766             } else {
767                 $next_cursor = isset($items[$end]->cursor) ?
768                     $items[$end]->cursor : $items[$end]->id;
769                 if ($end > $count) array_pop($items); // excess item.
770
771                 // check if there are more items for next page
772                 $fn_args = array_merge($args, array(0, 1, 0, $cursor));
773                 $more = call_user_func_array($fn, $fn_args);
774                 if (!$more->fetch() || empty($more)) {
775                     $next_cursor = 0;
776                 }
777             }
778
779             if (count($items) == $count+1) {
780                 // this means there is a previous page.
781                 $prev = array_shift($items);
782                 $prev_cursor = isset($prev->cursor) ?
783                     -$items[0]->cursor : -$items[0]->id;
784             }
785         } else if($cursor == -1) {
786             $fn_args = array_merge($args, array(0, $count+1));
787             $list = call_user_func_array($fn, $fn_args);
788
789             while($list->fetch()) {
790                 $items[] = clone($list);
791             }
792
793             if (count($items)==$count+1) {
794                 $next = array_pop($items);
795                 if(isset($next->cursor)) {
796                     $next_cursor = $items[$count-1]->cursor;
797                 } else {
798                     $next_cursor = $items[$count-1]->id;
799                 }
800             }
801
802         }
803         return array($items, $next_cursor, $prev_cursor);
804     }
805
806     /**
807      * save a collection of people tags into the cache
808      *
809      * @param string $ckey  cache key
810      * @param Profile_list &$tag the results to store
811      * @param integer $offset   offset for slicing results
812      * @param integer $limit    maximum number of results
813      *
814      * @return boolean success
815      */
816
817     static function setCache($ckey, &$tag, $offset=0, $limit=null) {
818         $cache = Cache::instance();
819         if (empty($cache)) {
820             return false;
821         }
822         $str = '';
823         $tags = array();
824         while ($tag->fetch()) {
825             $str .= $tag->tagger . ':' . $tag->tag . ';';
826             $tags[] = clone($tag);
827         }
828         $str = substr($str, 0, -1);
829         if ($offset>=0 && !is_null($limit)) {
830             $tags = array_slice($tags, $offset, $limit);
831         }
832
833         $tag = new ArrayWrapper($tags);
834
835         return self::cacheSet($ckey, $str);
836     }
837
838     /**
839      * get people tags from the cache
840      *
841      * @param string $ckey  cache key
842      * @param integer $offset   offset for slicing
843      * @param integer $limit    limit
844      *
845      * @return Profile_list results
846      */
847
848     static function getCached($ckey, $offset=0, $limit=null) {
849
850         $keys_str = self::cacheGet($ckey);
851         if ($keys_str === false) {
852             return false;
853         }
854
855         $pairs = explode(';', $keys_str);
856         $keys = array();
857         foreach ($pairs as $pair) {
858             $keys[] = explode(':', $pair);
859         }
860
861         if ($offset>=0 && !is_null($limit)) {
862             $keys = array_slice($keys, $offset, $limit);
863         }
864         return self::getByKeys($keys);
865     }
866
867     /**
868      * get Profile_list objects from the database
869      * given their (tag, tagger) key pairs.
870      *
871      * @param array $keys   array of array(tagger, tag)
872      *
873      * @return Profile_list results
874      */
875
876     static function getByKeys(array $keys) {
877         $cache = Cache::instance();
878
879         if (!empty($cache)) {
880             $tags = array();
881
882             foreach ($keys as $key) {
883                 $t = Profile_list::getByTaggerAndTag($key[0], $key[1]);
884                 if (!empty($t)) {
885                     $tags[] = $t;
886                 }
887             }
888             return new ArrayWrapper($tags);
889         } else {
890             $tag = new Profile_list();
891             if (empty($keys)) {
892                 //if no IDs requested, just return the tag object
893                 return $tag;
894             }
895
896             $pairs = array();
897             foreach ($keys as $key) {
898                 $pairs[] = '(' . $key[0] . ', "' . $key[1] . '")';
899             }
900
901             $tag->whereAdd('(tagger, tag) in (' . implode(', ', $pairs) . ')');
902
903             $tag->find();
904
905             $temp = array();
906
907             while ($tag->fetch()) {
908                 $temp[$tag->tagger.'-'.$tag->tag] = clone($tag);
909             }
910
911             $wrapped = array();
912
913             foreach ($keys as $key) {
914                 $id = $key[0].'-'.$key[1];
915                 if (array_key_exists($id, $temp)) {
916                     $wrapped[] = $temp[$id];
917                 }
918             }
919
920             return new ArrayWrapper($wrapped);
921         }
922     }
923
924     function insert()
925     {
926         $result = parent::insert();
927         if ($result) {
928             self::blow('profile:lists:%d', $this->tagger);
929         }
930         return $result;
931     }
932 }