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