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