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