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