]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - classes/Profile_list.php
Stronger typing, require array where param array
[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(255)  unique_key
47     public $mainpage;                        // varchar(255)
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' => 255, 'description' => 'universal identifier'),
68                 'mainpage' => array('type' => 'varchar', 'length' => 255, '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()
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();
353     }
354
355     /**
356      * Update a people tag gracefully
357      * also change "tag" fields in profile_tag table
358      *
359      * @param Profile_list $orig    Object's original form
360      *
361      * @return boolean success
362      */
363
364     function update($orig=null)
365     {
366         $result = true;
367
368         if (!is_object($orig) && !$orig instanceof Profile_list) {
369             parent::update($orig);
370         }
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($orig->tag != $this->tag || $orig->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($orig, $this);
385         }
386         parent::update($orig);
387         return $result;
388     }
389
390     /**
391      * return an xml string representing this people tag
392      * as the author of an atom feed
393      *
394      * @return string atom author element
395      */
396
397     function asAtomAuthor()
398     {
399         $xs = new XMLStringer(true);
400
401         $tagger = $this->getTagger();
402         $xs->elementStart('author');
403         $xs->element('name', null, '@' . $tagger->nickname . '/' . $this->tag);
404         $xs->element('uri', null, $this->permalink());
405         $xs->elementEnd('author');
406
407         return $xs->getString();
408     }
409
410     /**
411      * return an xml string to represent this people tag
412      * as the subject of an activitystreams feed.
413      *
414      * @return string activitystreams subject
415      */
416
417     function asActivitySubject()
418     {
419         return $this->asActivityNoun('subject');
420     }
421
422     /**
423      * return an xml string to represent this people tag
424      * as a noun in an activitystreams feed.
425      *
426      * @param string $element the xml tag
427      *
428      * @return string activitystreams noun
429      */
430
431     function asActivityNoun($element)
432     {
433         $noun = ActivityObject::fromPeopletag($this);
434         return $noun->asString('activity:' . $element);
435     }
436
437     /**
438      * get the cached number of profiles tagged with this
439      * people tag, re-count if the argument is true.
440      *
441      * @param boolean $recount  whether to ignore cache
442      *
443      * @return integer count
444      */
445
446     function taggedCount($recount=false)
447     {
448         $keypart = sprintf('profile_list:tagged_count:%d:%s', 
449                            $this->tagger,
450                            $this->tag);
451
452         $count = self::cacheGet($keypart);
453
454         if ($count === false) {
455             $tags = new Profile_tag();
456
457             $tags->tag = $this->tag;
458             $tags->tagger = $this->tagger;
459
460             $count = $tags->count('distinct tagged');
461
462             self::cacheSet($keypart, $count);
463         }
464
465         return $count;
466     }
467
468     /**
469      * get the cached number of profiles subscribed to this
470      * people tag, re-count if the argument is true.
471      *
472      * @param boolean $recount  whether to ignore cache
473      *
474      * @return integer count
475      */
476
477     function subscriberCount($recount=false)
478     {
479         $keypart = sprintf('profile_list:subscriber_count:%d', 
480                            $this->id);
481
482         $count = self::cacheGet($keypart);
483
484         if ($count === false) {
485
486             $sub = new Profile_tag_subscription();
487             $sub->profile_tag_id = $this->id;
488             $count = (int) $sub->count('distinct profile_id');
489
490             self::cacheSet($keypart, $count);
491         }
492
493         return $count;
494     }
495
496     /**
497      * get the cached number of profiles subscribed to this
498      * people tag, re-count if the argument is true.
499      *
500      * @param boolean $recount  whether to ignore cache
501      *
502      * @return integer count
503      */
504
505     function blowNoticeStreamCache($all=false)
506     {
507         self::blow('profile_list:notice_ids:%d', $this->id);
508         if ($all) {
509             self::blow('profile_list:notice_ids:%d;last', $this->id);
510         }
511     }
512
513     /**
514      * get the Profile_list object by the
515      * given tagger and with given tag
516      *
517      * @param integer $tagger   the id of the creator profile
518      * @param integer $tag      the tag
519      *
520      * @return integer count
521      */
522
523     static function getByTaggerAndTag($tagger, $tag)
524     {
525         $ptag = Profile_list::pkeyGet(array('tagger' => $tagger, 'tag' => $tag));
526         return $ptag;
527     }
528
529     /**
530      * create a profile_list record for a tag, tagger pair
531      * if it doesn't exist, return it.
532      *
533      * @param integer $tagger   the tagger
534      * @param string  $tag      the tag
535      * @param string  $description description
536      * @param boolean $private  protected or not
537      *
538      * @return Profile_list the people tag object
539      */
540
541     static function ensureTag($tagger, $tag, $description=null, $private=false)
542     {
543         $ptag = Profile_list::getByTaggerAndTag($tagger, $tag);
544
545         if(empty($ptag->id)) {
546             $args = array(
547                 'tag' => $tag,
548                 'tagger' => $tagger,
549                 'description' => $description,
550                 'private' => $private
551             );
552
553             $new_tag = Profile_list::saveNew($args);
554
555             return $new_tag;
556         }
557         return $ptag;
558     }
559
560     /**
561      * get the maximum number of characters
562      * that can be used in the description of
563      * a people tag.
564      *
565      * determined by $config['peopletag']['desclimit']
566      * if not set, falls back to $config['site']['textlimit']
567      *
568      * @return integer maximum number of characters
569      */
570
571     static function maxDescription()
572     {
573         $desclimit = common_config('peopletag', 'desclimit');
574         // null => use global limit (distinct from 0!)
575         if (is_null($desclimit)) {
576             $desclimit = common_config('site', 'textlimit');
577         }
578         return $desclimit;
579     }
580
581     /**
582      * check if the length of given text exceeds
583      * character limit.
584      *
585      * @param string $desc  the description
586      *
587      * @return boolean is the descripition too long?
588      */
589
590     static function descriptionTooLong($desc)
591     {
592         $desclimit = self::maxDescription();
593         return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
594     }
595
596     /**
597      * save a new people tag, this should be always used
598      * since it makes uri, homeurl, created and modified
599      * timestamps and performs checks.
600      *
601      * @param array $fields an array with fields and their values
602      *
603      * @return mixed Profile_list on success, false on fail
604      */
605     static function saveNew(array $fields) {
606         extract($fields);
607
608         $ptag = new Profile_list();
609
610         $ptag->query('BEGIN');
611
612         if (empty($tagger)) {
613             // TRANS: Server exception saving new tag without having a tagger specified.
614             throw new Exception(_('No tagger specified.'));
615         }
616
617         if (empty($tag)) {
618             // TRANS: Server exception saving new tag without having a tag specified.
619             throw new Exception(_('No tag specified.'));
620         }
621
622         if (empty($mainpage)) {
623             $mainpage = null;
624         }
625
626         if (empty($uri)) {
627             // fill in later...
628             $uri = null;
629         }
630
631         if (empty($mainpage)) {
632             $mainpage = null;
633         }
634
635         if (empty($description)) {
636             $description = null;
637         }
638
639         if (empty($private)) {
640             $private = false;
641         }
642
643         $ptag->tagger      = $tagger;
644         $ptag->tag         = $tag;
645         $ptag->description = $description;
646         $ptag->private     = $private;
647         $ptag->uri         = $uri;
648         $ptag->mainpage    = $mainpage;
649         $ptag->created     = common_sql_now();
650         $ptag->modified    = common_sql_now();
651
652         $result = $ptag->insert();
653
654         if (!$result) {
655             common_log_db_error($ptag, 'INSERT', __FILE__);
656             // TRANS: Server exception saving new tag.
657             throw new ServerException(_('Could not create profile tag.'));
658         }
659
660         if (!isset($uri) || empty($uri)) {
661             $orig = clone($ptag);
662             $ptag->uri = common_local_url('profiletagbyid', array('id' => $ptag->id, 'tagger_id' => $ptag->tagger));
663             $result = $ptag->update($orig);
664             if (!$result) {
665                 common_log_db_error($ptag, 'UPDATE', __FILE__);
666             // TRANS: Server exception saving new tag.
667                 throw new ServerException(_('Could not set profile tag URI.'));
668             }
669         }
670
671         if (!isset($mainpage) || empty($mainpage)) {
672             $orig = clone($ptag);
673             $user = User::getKV('id', $ptag->tagger);
674             if(!empty($user)) {
675                 $ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'tagger' => $user->nickname));
676             } else {
677                 $ptag->mainpage = $uri; // assume this is a remote peopletag and the uri works
678             }
679
680             $result = $ptag->update($orig);
681             if (!$result) {
682                 common_log_db_error($ptag, 'UPDATE', __FILE__);
683                 // TRANS: Server exception saving new tag.
684                 throw new ServerException(_('Could not set profile tag mainpage.'));
685             }
686         }
687         return $ptag;
688     }
689
690     /**
691      * get all items at given cursor position for api
692      *
693      * @param callback $fn  a function that takes the following arguments in order:
694      *                      $offset, $limit, $since_id, $max_id
695      *                      and returns a Profile_list object after making the DB query
696      * @param array $args   arguments required for $fn
697      * @param integer $cursor   the cursor
698      * @param integer $count    max. number of results
699      *
700      * Algorithm:
701      * - if cursor is 0, return empty list
702      * - if cursor is -1, get first 21 items, next_cursor = 20th prev_cursor = 0
703      * - if cursor is +ve get 22 consecutive items before starting at cursor
704      *   - return items[1..20] if items[0] == cursor else return items[0..21]
705      *   - prev_cursor = items[1]
706      *   - next_cursor = id of the last item being returned
707      *
708      * - if cursor is -ve get 22 consecutive items after cursor starting at cursor
709      *   - return items[1..20]
710      *
711      * @returns array (array (mixed items), int next_cursor, int previous_cursor)
712      */
713
714      // XXX: This should be in Memcached_DataObject... eventually.
715
716     static function getAtCursor($fn, array $args, $cursor, $count=20)
717     {
718         $items = array();
719
720         $since_id = 0;
721         $max_id = 0;
722         $next_cursor = 0;
723         $prev_cursor = 0;
724
725         if($cursor > 0) {
726             // if cursor is +ve fetch $count+2 items before cursor starting at cursor
727             $max_id = $cursor;
728             $fn_args = array_merge($args, array(0, $count+2, 0, $max_id));
729             $list = call_user_func_array($fn, $fn_args);
730             while($list->fetch()) {
731                 $items[] = clone($list);
732             }
733
734             if ((isset($items[0]->cursor) && $items[0]->cursor == $cursor) ||
735                 $items[0]->id == $cursor) {
736                 array_shift($items);
737                 $prev_cursor = isset($items[0]->cursor) ?
738                     -$items[0]->cursor : -$items[0]->id;
739             } else {
740                 if (count($items) > $count+1) {
741                     array_shift($items);
742                 }
743                 // this means the cursor item has been deleted, check to see if there are more
744                 $fn_args = array_merge($args, array(0, 1, $cursor));
745                 $more = call_user_func($fn, $fn_args);
746                 if (!$more->fetch() || empty($more)) {
747                     // no more items.
748                     $prev_cursor = 0;
749                 } else {
750                     $prev_cursor = isset($items[0]->cursor) ?
751                         -$items[0]->cursor : -$items[0]->id;
752                 }
753             }
754
755             if (count($items)==$count+1) {
756                 // this means there is a next page.
757                 $next = array_pop($items);
758                 $next_cursor = isset($next->cursor) ?
759                     $items[$count-1]->cursor : $items[$count-1]->id;
760             }
761
762         } else if($cursor < -1) {
763             // if cursor is -ve fetch $count+2 items created after -$cursor-1
764             $cursor = abs($cursor);
765             $since_id = $cursor-1;
766
767             $fn_args = array_merge($args, array(0, $count+2, $since_id));
768             $list = call_user_func_array($fn, $fn_args);
769             while($list->fetch()) {
770                 $items[] = clone($list);
771             }
772
773             $end = count($items)-1;
774             if ((isset($items[$end]->cursor) && $items[$end]->cursor == $cursor) ||
775                 $items[$end]->id == $cursor) {
776                 array_pop($items);
777                 $next_cursor = isset($items[$end-1]->cursor) ?
778                     $items[$end-1]->cursor : $items[$end-1]->id;
779             } else {
780                 $next_cursor = isset($items[$end]->cursor) ?
781                     $items[$end]->cursor : $items[$end]->id;
782                 if ($end > $count) array_pop($items); // excess item.
783
784                 // check if there are more items for next page
785                 $fn_args = array_merge($args, array(0, 1, 0, $cursor));
786                 $more = call_user_func_array($fn, $fn_args);
787                 if (!$more->fetch() || empty($more)) {
788                     $next_cursor = 0;
789                 }
790             }
791
792             if (count($items) == $count+1) {
793                 // this means there is a previous page.
794                 $prev = array_shift($items);
795                 $prev_cursor = isset($prev->cursor) ?
796                     -$items[0]->cursor : -$items[0]->id;
797             }
798         } else if($cursor == -1) {
799             $fn_args = array_merge($args, array(0, $count+1));
800             $list = call_user_func_array($fn, $fn_args);
801
802             while($list->fetch()) {
803                 $items[] = clone($list);
804             }
805
806             if (count($items)==$count+1) {
807                 $next = array_pop($items);
808                 if(isset($next->cursor)) {
809                     $next_cursor = $items[$count-1]->cursor;
810                 } else {
811                     $next_cursor = $items[$count-1]->id;
812                 }
813             }
814
815         }
816         return array($items, $next_cursor, $prev_cursor);
817     }
818
819     /**
820      * save a collection of people tags into the cache
821      *
822      * @param string $ckey  cache key
823      * @param Profile_list &$tag the results to store
824      * @param integer $offset   offset for slicing results
825      * @param integer $limit    maximum number of results
826      *
827      * @return boolean success
828      */
829
830     static function setCache($ckey, &$tag, $offset=0, $limit=null) {
831         $cache = Cache::instance();
832         if (empty($cache)) {
833             return false;
834         }
835         $str = '';
836         $tags = array();
837         while ($tag->fetch()) {
838             $str .= $tag->tagger . ':' . $tag->tag . ';';
839             $tags[] = clone($tag);
840         }
841         $str = substr($str, 0, -1);
842         if ($offset>=0 && !is_null($limit)) {
843             $tags = array_slice($tags, $offset, $limit);
844         }
845
846         $tag = new ArrayWrapper($tags);
847
848         return self::cacheSet($ckey, $str);
849     }
850
851     /**
852      * get people tags from the cache
853      *
854      * @param string $ckey  cache key
855      * @param integer $offset   offset for slicing
856      * @param integer $limit    limit
857      *
858      * @return Profile_list results
859      */
860
861     static function getCached($ckey, $offset=0, $limit=null) {
862
863         $keys_str = self::cacheGet($ckey);
864         if ($keys_str === false) {
865             return false;
866         }
867
868         $pairs = explode(';', $keys_str);
869         $keys = array();
870         foreach ($pairs as $pair) {
871             $keys[] = explode(':', $pair);
872         }
873
874         if ($offset>=0 && !is_null($limit)) {
875             $keys = array_slice($keys, $offset, $limit);
876         }
877         return self::getByKeys($keys);
878     }
879
880     /**
881      * get Profile_list objects from the database
882      * given their (tag, tagger) key pairs.
883      *
884      * @param array $keys   array of array(tagger, tag)
885      *
886      * @return Profile_list results
887      */
888
889     static function getByKeys(array $keys) {
890         $cache = Cache::instance();
891
892         if (!empty($cache)) {
893             $tags = array();
894
895             foreach ($keys as $key) {
896                 $t = Profile_list::getByTaggerAndTag($key[0], $key[1]);
897                 if (!empty($t)) {
898                     $tags[] = $t;
899                 }
900             }
901             return new ArrayWrapper($tags);
902         } else {
903             $tag = new Profile_list();
904             if (empty($keys)) {
905                 //if no IDs requested, just return the tag object
906                 return $tag;
907             }
908
909             $pairs = array();
910             foreach ($keys as $key) {
911                 $pairs[] = '(' . $key[0] . ', "' . $key[1] . '")';
912             }
913
914             $tag->whereAdd('(tagger, tag) in (' . implode(', ', $pairs) . ')');
915
916             $tag->find();
917
918             $temp = array();
919
920             while ($tag->fetch()) {
921                 $temp[$tag->tagger.'-'.$tag->tag] = clone($tag);
922             }
923
924             $wrapped = array();
925
926             foreach ($keys as $key) {
927                 $id = $key[0].'-'.$key[1];
928                 if (array_key_exists($id, $temp)) {
929                     $wrapped[] = $temp[$id];
930                 }
931             }
932
933             return new ArrayWrapper($wrapped);
934         }
935     }
936
937     function insert()
938     {
939         $result = parent::insert();
940         if ($result) {
941             self::blow('profile:lists:%d', $this->tagger);
942         }
943         return $result;
944     }
945 }