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