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