]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - classes/Profile.php
44e99b1d326f48e3dc14bec9c480b91fd8722c95
[quix0rs-gnu-social.git] / classes / Profile.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2008, 2009, StatusNet, Inc.
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Affero General Public License for more details.
15  *
16  * You should have received a copy of the GNU Affero General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
21
22 /**
23  * Table Definition for profile
24  */
25 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
26
27 class Profile extends Memcached_DataObject
28 {
29     ###START_AUTOCODE
30     /* the code below is auto generated do not remove the above tag */
31
32     public $__table = 'profile';                         // table name
33     public $id;                              // int(4)  primary_key not_null
34     public $nickname;                        // varchar(64)  multiple_key not_null
35     public $fullname;                        // varchar(255)  multiple_key
36     public $profileurl;                      // varchar(255)
37     public $homepage;                        // varchar(255)  multiple_key
38     public $bio;                             // text()  multiple_key
39     public $location;                        // varchar(255)  multiple_key
40     public $lat;                             // decimal(10,7)
41     public $lon;                             // decimal(10,7)
42     public $location_id;                     // int(4)
43     public $location_ns;                     // int(4)
44     public $created;                         // datetime()   not_null
45     public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
46
47     /* Static get */
48     function staticGet($k,$v=NULL) {
49         return Memcached_DataObject::staticGet('Profile',$k,$v);
50     }
51
52     /* the code above is auto generated do not remove the tag below */
53     ###END_AUTOCODE
54
55     function getUser()
56     {
57         return User::staticGet('id', $this->id);
58     }
59
60     function getAvatar($width, $height=null)
61     {
62         if (is_null($height)) {
63             $height = $width;
64         }
65         return Avatar::pkeyGet(array('profile_id' => $this->id,
66                                      'width' => $width,
67                                      'height' => $height));
68     }
69
70     function getOriginalAvatar()
71     {
72         $avatar = DB_DataObject::factory('avatar');
73         $avatar->profile_id = $this->id;
74         $avatar->original = true;
75         if ($avatar->find(true)) {
76             return $avatar;
77         } else {
78             return null;
79         }
80     }
81
82     function setOriginal($filename)
83     {
84         $imagefile = new ImageFile($this->id, Avatar::path($filename));
85
86         $avatar = new Avatar();
87         $avatar->profile_id = $this->id;
88         $avatar->width = $imagefile->width;
89         $avatar->height = $imagefile->height;
90         $avatar->mediatype = image_type_to_mime_type($imagefile->type);
91         $avatar->filename = $filename;
92         $avatar->original = true;
93         $avatar->url = Avatar::url($filename);
94         $avatar->created = DB_DataObject_Cast::dateTime(); # current time
95
96         // XXX: start a transaction here
97
98         if (!$this->delete_avatars() || !$avatar->insert()) {
99             @unlink(Avatar::path($filename));
100             return null;
101         }
102
103         foreach (array(AVATAR_PROFILE_SIZE, AVATAR_STREAM_SIZE, AVATAR_MINI_SIZE) as $size) {
104             // We don't do a scaled one if original is our scaled size
105             if (!($avatar->width == $size && $avatar->height == $size)) {
106                 $scaled_filename = $imagefile->resize($size);
107
108                 //$scaled = DB_DataObject::factory('avatar');
109                 $scaled = new Avatar();
110                 $scaled->profile_id = $this->id;
111                 $scaled->width = $size;
112                 $scaled->height = $size;
113                 $scaled->original = false;
114                 $scaled->mediatype = image_type_to_mime_type($imagefile->type);
115                 $scaled->filename = $scaled_filename;
116                 $scaled->url = Avatar::url($scaled_filename);
117                 $scaled->created = DB_DataObject_Cast::dateTime(); # current time
118
119                 if (!$scaled->insert()) {
120                     return null;
121                 }
122             }
123         }
124
125         return $avatar;
126     }
127
128     /**
129      * Delete attached avatars for this user from the database and filesystem.
130      * This should be used instead of a batch delete() to ensure that files
131      * get removed correctly.
132      *
133      * @param boolean $original true to delete only the original-size file
134      * @return <type>
135      */
136     function delete_avatars($original=true)
137     {
138         $avatar = new Avatar();
139         $avatar->profile_id = $this->id;
140         $avatar->find();
141         while ($avatar->fetch()) {
142             if ($avatar->original) {
143                 if ($original == false) {
144                     continue;
145                 }
146             }
147             $avatar->delete();
148         }
149         return true;
150     }
151
152     /**
153      * Gets either the full name (if filled) or the nickname.
154      *
155      * @return string
156      */
157     function getBestName()
158     {
159         return ($this->fullname) ? $this->fullname : $this->nickname;
160     }
161
162     /**
163      * Gets the full name (if filled) with nickname as a parenthetical, or the nickname alone
164      * if no fullname is provided.
165      *
166      * @return string
167      */
168     function getFancyName()
169     {
170         if ($this->fullname) {
171             // TRANS: Full name of a profile or group (%1$s) followed by nickname (%2$s) in parentheses.
172             return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname);
173         } else {
174             return $this->nickname;
175         }
176     }
177
178     /**
179      * Get the most recent notice posted by this user, if any.
180      *
181      * @return mixed Notice or null
182      */
183     function getCurrentNotice()
184     {
185         $notice = $this->getNotices(0, 1);
186
187         if ($notice->fetch()) {
188             if ($notice instanceof ArrayWrapper) {
189                 // hack for things trying to work with single notices
190                 return $notice->_items[0];
191             }
192             return $notice;
193         } else {
194             return null;
195         }
196     }
197
198     function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
199     {
200         $stream = new TaggedProfileNoticeStream($this, $tag);
201
202         return $stream->getNotices($offset, $limit, $since_id, $max_id);
203     }
204
205     function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
206     {
207         $stream = new ProfileNoticeStream($this);
208
209         return $stream->getNotices($offset, $limit, $since_id, $max_id);
210     }
211
212     function isMember($group)
213     {
214         $gm = Group_member::pkeyGet(array('profile_id' => $this->id,
215                                           'group_id' => $group->id));
216         return (!empty($gm));
217     }
218
219     function isAdmin($group)
220     {
221         $gm = Group_member::pkeyGet(array('profile_id' => $this->id,
222                                           'group_id' => $group->id));
223         return (!empty($gm) && $gm->is_admin);
224     }
225
226     function isPendingMember($group)
227     {
228         $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id,
229                                                    'group_id' => $group->id));
230         return !empty($request);
231     }
232
233     function getGroups($offset=0, $limit=PROFILES_PER_PAGE)
234     {
235         $ids = array();
236         
237         $keypart = sprintf('profile:groups:%d', $this->id);
238
239         $idstring = self::cacheGet($keypart);
240
241         if ($idstring !== false) {
242             $ids = explode(',', $idstring);
243         } else {
244             $gm = new Group_member();
245
246             $gm->profile_id = $this->id;
247
248             if ($gm->find()) {
249                 while ($gm->fetch()) {
250                     $ids[] = $gm->group_id;
251                 }
252             }
253
254             self::cacheSet($keypart, implode(',', $ids));
255         }
256
257         $groups = array();
258
259         foreach ($ids as $id) {
260             $group = User_group::staticGet('id', $id);
261             if (!empty($group)) {
262                 $groups[] = $group;
263             }
264         }
265
266         return new ArrayWrapper($groups);
267     }
268
269     function isTagged($peopletag)
270     {
271         $tag = Profile_tag::pkeyGet(array('tagger' => $peopletag->tagger,
272                                           'tagged' => $this->id,
273                                           'tag'    => $peopletag->tag));
274         return !empty($tag);
275     }
276
277     function canTag($tagged)
278     {
279         if (empty($tagged)) {
280             return false;
281         }
282
283         if ($tagged->id == $this->id) {
284             return true;
285         }
286
287         $all = common_config('peopletag', 'allow_tagging', 'all');
288         $local = common_config('peopletag', 'allow_tagging', 'local');
289         $remote = common_config('peopletag', 'allow_tagging', 'remote');
290         $subs = common_config('peopletag', 'allow_tagging', 'subs');
291
292         if ($all) {
293             return true;
294         }
295
296         $tagged_user = $tagged->getUser();
297         if (!empty($tagged_user)) {
298             if ($local) {
299                 return true;
300             }
301         } else if ($subs) {
302             return (Subscription::exists($this, $tagged) ||
303                     Subscription::exists($tagged, $this));
304         } else if ($remote) {
305             return true;
306         }
307         return false;
308     }
309
310     function getOwnedTags($auth_user, $offset=0, $limit=null, $since_id=0, $max_id=0)
311     {
312         $tags = new Profile_list();
313         $tags->tagger = $this->id;
314
315         if (($auth_user instanceof User || $auth_user instanceof Profile) &&
316                 $auth_user->id === $this->id) {
317             // no condition, get both private and public tags
318         } else {
319             $tags->private = false;
320         }
321
322         $tags->selectAdd('id as "cursor"');
323
324         if ($since_id>0) {
325            $tags->whereAdd('id > '.$since_id);
326         }
327
328         if ($max_id>0) {
329             $tags->whereAdd('id <= '.$max_id);
330         }
331
332         if($offset>=0 && !is_null($limit)) {
333             $tags->limit($offset, $limit);
334         }
335
336         $tags->orderBy('id DESC');
337         $tags->find();
338
339         return $tags;
340     }
341
342     function getOtherTags($auth_user=null, $offset=0, $limit=null, $since_id=0, $max_id=0)
343     {
344         $lists = new Profile_list();
345
346         $tags = new Profile_tag();
347         $tags->tagged = $this->id;
348
349         $lists->joinAdd($tags);
350         #@fixme: postgres (round(date_part('epoch', my_date)))
351         $lists->selectAdd('unix_timestamp(profile_tag.modified) as "cursor"');
352
353         if ($auth_user instanceof User || $auth_user instanceof Profile) {
354             $lists->whereAdd('( ( profile_list.private = false ) ' .
355                              'OR ( profile_list.tagger = ' . $auth_user->id . ' AND ' .
356                              'profile_list.private = true ) )');
357         } else {
358             $lists->private = false;
359         }
360
361         if ($since_id>0) {
362            $lists->whereAdd('cursor > '.$since_id);
363         }
364
365         if ($max_id>0) {
366             $lists->whereAdd('cursor <= '.$max_id);
367         }
368
369         if($offset>=0 && !is_null($limit)) {
370             $lists->limit($offset, $limit);
371         }
372
373         $lists->orderBy('profile_tag.modified DESC');
374         $lists->find();
375
376         return $lists;
377     }
378
379     function getPrivateTags($offset=0, $limit=null, $since_id=0, $max_id=0)
380     {
381         $tags = new Profile_list();
382         $tags->private = true;
383         $tags->tagger = $this->id;
384
385         if ($since_id>0) {
386            $tags->whereAdd('id > '.$since_id);
387         }
388
389         if ($max_id>0) {
390             $tags->whereAdd('id <= '.$max_id);
391         }
392
393         if($offset>=0 && !is_null($limit)) {
394             $tags->limit($offset, $limit);
395         }
396
397         $tags->orderBy('id DESC');
398         $tags->find();
399
400         return $tags;
401     }
402
403     function hasLocalTags()
404     {
405         $tags = new Profile_tag();
406
407         $tags->joinAdd(array('tagger', 'user:id'));
408         $tags->whereAdd('tagged  = '.$this->id);
409         $tags->whereAdd('tagger != '.$this->id);
410
411         $tags->limit(0, 1);
412         $tags->fetch();
413
414         return ($tags->N == 0) ? false : true;
415     }
416
417     function getTagSubscriptions($offset=0, $limit=null, $since_id=0, $max_id=0)
418     {
419         $lists = new Profile_list();
420         $subs = new Profile_tag_subscription();
421
422         $lists->joinAdd($subs);
423         #@fixme: postgres (round(date_part('epoch', my_date)))
424         $lists->selectAdd('unix_timestamp(profile_tag_subscription.created) as "cursor"');
425
426         $lists->whereAdd('profile_tag_subscription.profile_id = '.$this->id);
427
428         if ($since_id>0) {
429            $lists->whereAdd('cursor > '.$since_id);
430         }
431
432         if ($max_id>0) {
433             $lists->whereAdd('cursor <= '.$max_id);
434         }
435
436         if($offset>=0 && !is_null($limit)) {
437             $lists->limit($offset, $limit);
438         }
439
440         $lists->orderBy('"cursor" DESC');
441         $lists->find();
442
443         return $lists;
444     }
445
446     /**
447      * Request to join the given group.
448      * May throw exceptions on failure.
449      *
450      * @param User_group $group
451      * @return mixed: Group_member on success, Group_join_queue if pending approval, null on some cancels?
452      */
453     function joinGroup(User_group $group)
454     {
455         $join = null;
456         if ($group->join_policy == User_group::JOIN_POLICY_MODERATE) {
457             $join = Group_join_queue::saveNew($this, $group);
458         } else {
459             if (Event::handle('StartJoinGroup', array($group, $this))) {
460                 $join = Group_member::join($group->id, $this->id);
461                 self::blow('profile:groups:%d', $this->id);
462                 Event::handle('EndJoinGroup', array($group, $this));
463             }
464         }
465         if ($join) {
466             // Send any applicable notifications...
467             $join->notify();
468         }
469         return $join;
470     }
471
472     /**
473      * Leave a group that this profile is a member of.
474      *
475      * @param User_group $group
476      */
477     function leaveGroup(User_group $group)
478     {
479         if (Event::handle('StartLeaveGroup', array($group, $this))) {
480             Group_member::leave($group->id, $this->id);
481             self::blow('profile:groups:%d', $this->id);
482             Event::handle('EndLeaveGroup', array($group, $this));
483         }
484     }
485
486     function avatarUrl($size=AVATAR_PROFILE_SIZE)
487     {
488         $avatar = $this->getAvatar($size);
489         if ($avatar) {
490             return $avatar->displayUrl();
491         } else {
492             return Avatar::defaultImage($size);
493         }
494     }
495
496     function getSubscriptions($offset=0, $limit=null)
497     {
498         $subs = Subscription::bySubscriber($this->id,
499                                            $offset,
500                                            $limit);
501
502         $profiles = array();
503
504         while ($subs->fetch()) {
505             $profile = Profile::staticGet($subs->subscribed);
506             if ($profile) {
507                 $profiles[] = $profile;
508             }
509         }
510
511         return new ArrayWrapper($profiles);
512     }
513
514     function getSubscribers($offset=0, $limit=null)
515     {
516         $subs = Subscription::bySubscribed($this->id,
517                                            $offset,
518                                            $limit);
519
520         $profiles = array();
521
522         while ($subs->fetch()) {
523             $profile = Profile::staticGet($subs->subscriber);
524             if ($profile) {
525                 $profiles[] = $profile;
526             }
527         }
528
529         return new ArrayWrapper($profiles);
530     }
531
532     function getTaggedSubscribers($tag)
533     {
534         $qry =
535           'SELECT profile.* ' .
536           'FROM profile JOIN (subscription, profile_tag, profile_list) ' .
537           'ON profile.id = subscription.subscriber ' .
538           'AND profile.id = profile_tag.tagged ' .
539           'AND profile_tag.tagger = profile_list.tagger AND profile_tag.tag = profile_list.tag ' .
540           'WHERE subscription.subscribed = %d ' .
541           'AND subscription.subscribed != subscription.subscriber ' .
542           'AND profile_tag.tagger = %d AND profile_tag.tag = "%s" ' .
543           'AND profile_list.private = false ' .
544           'ORDER BY subscription.created DESC';
545
546         $profile = new Profile();
547         $tagged = array();
548
549         $cnt = $profile->query(sprintf($qry, $this->id, $this->id, $tag));
550
551         while ($profile->fetch()) {
552             $tagged[] = clone($profile);
553         }
554         return $tagged;
555     }
556
557     /**
558      * Get pending subscribers, who have not yet been approved.
559      *
560      * @param int $offset
561      * @param int $limit
562      * @return Profile
563      */
564     function getRequests($offset=0, $limit=null)
565     {
566         $qry =
567           'SELECT profile.* ' .
568           'FROM profile JOIN subscription_queue '.
569           'ON profile.id = subscription_queue.subscriber ' .
570           'WHERE subscription_queue.subscribed = %d ' .
571           'ORDER BY subscription_queue.created DESC ';
572
573         if ($limit != null) {
574             if (common_config('db','type') == 'pgsql') {
575                 $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
576             } else {
577                 $qry .= ' LIMIT ' . $offset . ', ' . $limit;
578             }
579         }
580
581         $members = new Profile();
582
583         $members->query(sprintf($qry, $this->id));
584         return $members;
585     }
586
587     function subscriptionCount()
588     {
589         $c = Cache::instance();
590
591         if (!empty($c)) {
592             $cnt = $c->get(Cache::key('profile:subscription_count:'.$this->id));
593             if (is_integer($cnt)) {
594                 return (int) $cnt;
595             }
596         }
597
598         $sub = new Subscription();
599         $sub->subscriber = $this->id;
600
601         $cnt = (int) $sub->count('distinct subscribed');
602
603         $cnt = ($cnt > 0) ? $cnt - 1 : $cnt;
604
605         if (!empty($c)) {
606             $c->set(Cache::key('profile:subscription_count:'.$this->id), $cnt);
607         }
608
609         return $cnt;
610     }
611
612     function subscriberCount()
613     {
614         $c = Cache::instance();
615         if (!empty($c)) {
616             $cnt = $c->get(Cache::key('profile:subscriber_count:'.$this->id));
617             if (is_integer($cnt)) {
618                 return (int) $cnt;
619             }
620         }
621
622         $sub = new Subscription();
623         $sub->subscribed = $this->id;
624         $sub->whereAdd('subscriber != subscribed');
625         $cnt = (int) $sub->count('distinct subscriber');
626
627         if (!empty($c)) {
628             $c->set(Cache::key('profile:subscriber_count:'.$this->id), $cnt);
629         }
630
631         return $cnt;
632     }
633
634     /**
635      * Is this profile subscribed to another profile?
636      *
637      * @param Profile $other
638      * @return boolean
639      */
640     function isSubscribed($other)
641     {
642         return Subscription::exists($this, $other);
643     }
644
645     /**
646      * Check if a pending subscription request is outstanding for this...
647      *
648      * @param Profile $other
649      * @return boolean
650      */
651     function hasPendingSubscription($other)
652     {
653         return Subscription_queue::exists($this, $other);
654     }
655
656     /**
657      * Are these two profiles subscribed to each other?
658      *
659      * @param Profile $other
660      * @return boolean
661      */
662     function mutuallySubscribed($other)
663     {
664         return $this->isSubscribed($other) &&
665           $other->isSubscribed($this);
666     }
667
668     function hasFave($notice)
669     {
670         $cache = Cache::instance();
671
672         // XXX: Kind of a hack.
673
674         if (!empty($cache)) {
675             // This is the stream of favorite notices, in rev chron
676             // order. This forces it into cache.
677
678             $ids = Fave::idStream($this->id, 0, CachingNoticeStream::CACHE_WINDOW);
679
680             // If it's in the list, then it's a fave
681
682             if (in_array($notice->id, $ids)) {
683                 return true;
684             }
685
686             // If we're not past the end of the cache window,
687             // then the cache has all available faves, so this one
688             // is not a fave.
689
690             if (count($ids) < CachingNoticeStream::CACHE_WINDOW) {
691                 return false;
692             }
693
694             // Otherwise, cache doesn't have all faves;
695             // fall through to the default
696         }
697
698         $fave = Fave::pkeyGet(array('user_id' => $this->id,
699                                     'notice_id' => $notice->id));
700         return ((is_null($fave)) ? false : true);
701     }
702
703     function faveCount()
704     {
705         $c = Cache::instance();
706         if (!empty($c)) {
707             $cnt = $c->get(Cache::key('profile:fave_count:'.$this->id));
708             if (is_integer($cnt)) {
709                 return (int) $cnt;
710             }
711         }
712
713         $faves = new Fave();
714         $faves->user_id = $this->id;
715         $cnt = (int) $faves->count('distinct notice_id');
716
717         if (!empty($c)) {
718             $c->set(Cache::key('profile:fave_count:'.$this->id), $cnt);
719         }
720
721         return $cnt;
722     }
723
724     function noticeCount()
725     {
726         $c = Cache::instance();
727
728         if (!empty($c)) {
729             $cnt = $c->get(Cache::key('profile:notice_count:'.$this->id));
730             if (is_integer($cnt)) {
731                 return (int) $cnt;
732             }
733         }
734
735         $notices = new Notice();
736         $notices->profile_id = $this->id;
737         $cnt = (int) $notices->count('distinct id');
738
739         if (!empty($c)) {
740             $c->set(Cache::key('profile:notice_count:'.$this->id), $cnt);
741         }
742
743         return $cnt;
744     }
745
746     function blowFavesCache()
747     {
748         $cache = Cache::instance();
749         if ($cache) {
750             // Faves don't happen chronologically, so we need to blow
751             // ;last cache, too
752             $cache->delete(Cache::key('fave:ids_by_user:'.$this->id));
753             $cache->delete(Cache::key('fave:ids_by_user:'.$this->id.';last'));
754             $cache->delete(Cache::key('fave:ids_by_user_own:'.$this->id));
755             $cache->delete(Cache::key('fave:ids_by_user_own:'.$this->id.';last'));
756         }
757         $this->blowFaveCount();
758     }
759
760     function blowSubscriberCount()
761     {
762         $c = Cache::instance();
763         if (!empty($c)) {
764             $c->delete(Cache::key('profile:subscriber_count:'.$this->id));
765         }
766     }
767
768     function blowSubscriptionCount()
769     {
770         $c = Cache::instance();
771         if (!empty($c)) {
772             $c->delete(Cache::key('profile:subscription_count:'.$this->id));
773         }
774     }
775
776     function blowFaveCount()
777     {
778         $c = Cache::instance();
779         if (!empty($c)) {
780             $c->delete(Cache::key('profile:fave_count:'.$this->id));
781         }
782     }
783
784     function blowNoticeCount()
785     {
786         $c = Cache::instance();
787         if (!empty($c)) {
788             $c->delete(Cache::key('profile:notice_count:'.$this->id));
789         }
790     }
791
792     static function maxBio()
793     {
794         $biolimit = common_config('profile', 'biolimit');
795         // null => use global limit (distinct from 0!)
796         if (is_null($biolimit)) {
797             $biolimit = common_config('site', 'textlimit');
798         }
799         return $biolimit;
800     }
801
802     static function bioTooLong($bio)
803     {
804         $biolimit = self::maxBio();
805         return ($biolimit > 0 && !empty($bio) && (mb_strlen($bio) > $biolimit));
806     }
807
808     function delete()
809     {
810         $this->_deleteNotices();
811         $this->_deleteSubscriptions();
812         $this->_deleteMessages();
813         $this->_deleteTags();
814         $this->_deleteBlocks();
815         $this->delete_avatars();
816
817         // Warning: delete() will run on the batch objects,
818         // not on individual objects.
819         $related = array('Reply',
820                          'Group_member',
821                          );
822         Event::handle('ProfileDeleteRelated', array($this, &$related));
823
824         foreach ($related as $cls) {
825             $inst = new $cls();
826             $inst->profile_id = $this->id;
827             $inst->delete();
828         }
829
830         parent::delete();
831     }
832
833     function _deleteNotices()
834     {
835         $notice = new Notice();
836         $notice->profile_id = $this->id;
837
838         if ($notice->find()) {
839             while ($notice->fetch()) {
840                 $other = clone($notice);
841                 $other->delete();
842             }
843         }
844     }
845
846     function _deleteSubscriptions()
847     {
848         $sub = new Subscription();
849         $sub->subscriber = $this->id;
850
851         $sub->find();
852
853         while ($sub->fetch()) {
854             $other = Profile::staticGet('id', $sub->subscribed);
855             if (empty($other)) {
856                 continue;
857             }
858             if ($other->id == $this->id) {
859                 continue;
860             }
861             Subscription::cancel($this, $other);
862         }
863
864         $subd = new Subscription();
865         $subd->subscribed = $this->id;
866         $subd->find();
867
868         while ($subd->fetch()) {
869             $other = Profile::staticGet('id', $subd->subscriber);
870             if (empty($other)) {
871                 continue;
872             }
873             if ($other->id == $this->id) {
874                 continue;
875             }
876             Subscription::cancel($other, $this);
877         }
878
879         $self = new Subscription();
880
881         $self->subscriber = $this->id;
882         $self->subscribed = $this->id;
883
884         $self->delete();
885     }
886
887     function _deleteMessages()
888     {
889         $msg = new Message();
890         $msg->from_profile = $this->id;
891         $msg->delete();
892
893         $msg = new Message();
894         $msg->to_profile = $this->id;
895         $msg->delete();
896     }
897
898     function _deleteTags()
899     {
900         $tag = new Profile_tag();
901         $tag->tagged = $this->id;
902         $tag->delete();
903     }
904
905     function _deleteBlocks()
906     {
907         $block = new Profile_block();
908         $block->blocked = $this->id;
909         $block->delete();
910
911         $block = new Group_block();
912         $block->blocked = $this->id;
913         $block->delete();
914     }
915
916     // XXX: identical to Notice::getLocation.
917
918     function getLocation()
919     {
920         $location = null;
921
922         if (!empty($this->location_id) && !empty($this->location_ns)) {
923             $location = Location::fromId($this->location_id, $this->location_ns);
924         }
925
926         if (is_null($location)) { // no ID, or Location::fromId() failed
927             if (!empty($this->lat) && !empty($this->lon)) {
928                 $location = Location::fromLatLon($this->lat, $this->lon);
929             }
930         }
931
932         if (is_null($location)) { // still haven't found it!
933             if (!empty($this->location)) {
934                 $location = Location::fromName($this->location);
935             }
936         }
937
938         return $location;
939     }
940
941     function hasRole($name)
942     {
943         $has_role = false;
944         if (Event::handle('StartHasRole', array($this, $name, &$has_role))) {
945             $role = Profile_role::pkeyGet(array('profile_id' => $this->id,
946                                                 'role' => $name));
947             $has_role = !empty($role);
948             Event::handle('EndHasRole', array($this, $name, $has_role));
949         }
950         return $has_role;
951     }
952
953     function grantRole($name)
954     {
955         if (Event::handle('StartGrantRole', array($this, $name))) {
956
957             $role = new Profile_role();
958
959             $role->profile_id = $this->id;
960             $role->role       = $name;
961             $role->created    = common_sql_now();
962
963             $result = $role->insert();
964
965             if (!$result) {
966                 throw new Exception("Can't save role '$name' for profile '{$this->id}'");
967             }
968
969             if ($name == 'owner') {
970                 User::blow('user:site_owner');
971             }
972
973             Event::handle('EndGrantRole', array($this, $name));
974         }
975
976         return $result;
977     }
978
979     function revokeRole($name)
980     {
981         if (Event::handle('StartRevokeRole', array($this, $name))) {
982
983             $role = Profile_role::pkeyGet(array('profile_id' => $this->id,
984                                                 'role' => $name));
985
986             if (empty($role)) {
987                 // TRANS: Exception thrown when trying to revoke an existing role for a user that does not exist.
988                 // TRANS: %1$s is the role name, %2$s is the user ID (number).
989                 throw new Exception(sprintf(_('Cannot revoke role "%1$s" for user #%2$d; does not exist.'),$name, $this->id));
990             }
991
992             $result = $role->delete();
993
994             if (!$result) {
995                 common_log_db_error($role, 'DELETE', __FILE__);
996                 // TRANS: Exception thrown when trying to revoke a role for a user with a failing database query.
997                 // TRANS: %1$s is the role name, %2$s is the user ID (number).
998                 throw new Exception(sprintf(_('Cannot revoke role "%1$s" for user #%2$d; database error.'),$name, $this->id));
999             }
1000
1001             if ($name == 'owner') {
1002                 User::blow('user:site_owner');
1003             }
1004
1005             Event::handle('EndRevokeRole', array($this, $name));
1006
1007             return true;
1008         }
1009     }
1010
1011     function isSandboxed()
1012     {
1013         return $this->hasRole(Profile_role::SANDBOXED);
1014     }
1015
1016     function isSilenced()
1017     {
1018         return $this->hasRole(Profile_role::SILENCED);
1019     }
1020
1021     function sandbox()
1022     {
1023         $this->grantRole(Profile_role::SANDBOXED);
1024     }
1025
1026     function unsandbox()
1027     {
1028         $this->revokeRole(Profile_role::SANDBOXED);
1029     }
1030
1031     function silence()
1032     {
1033         $this->grantRole(Profile_role::SILENCED);
1034     }
1035
1036     function unsilence()
1037     {
1038         $this->revokeRole(Profile_role::SILENCED);
1039     }
1040
1041     /**
1042      * Does this user have the right to do X?
1043      *
1044      * With our role-based authorization, this is merely a lookup for whether the user
1045      * has a particular role. The implementation currently uses a switch statement
1046      * to determine if the user has the pre-defined role to exercise the right. Future
1047      * implementations may allow per-site roles, and different mappings of roles to rights.
1048      *
1049      * @param $right string Name of the right, usually a constant in class Right
1050      * @return boolean whether the user has the right in question
1051      */
1052     function hasRight($right)
1053     {
1054         $result = false;
1055
1056         if ($this->hasRole(Profile_role::DELETED)) {
1057             return false;
1058         }
1059
1060         if (Event::handle('UserRightsCheck', array($this, $right, &$result))) {
1061             switch ($right)
1062             {
1063             case Right::DELETEOTHERSNOTICE:
1064             case Right::MAKEGROUPADMIN:
1065             case Right::SANDBOXUSER:
1066             case Right::SILENCEUSER:
1067             case Right::DELETEUSER:
1068             case Right::DELETEGROUP:
1069                 $result = $this->hasRole(Profile_role::MODERATOR);
1070                 break;
1071             case Right::CONFIGURESITE:
1072                 $result = $this->hasRole(Profile_role::ADMINISTRATOR);
1073                 break;
1074             case Right::GRANTROLE:
1075             case Right::REVOKEROLE:
1076                 $result = $this->hasRole(Profile_role::OWNER);
1077                 break;
1078             case Right::NEWNOTICE:
1079             case Right::NEWMESSAGE:
1080             case Right::SUBSCRIBE:
1081             case Right::CREATEGROUP:
1082                 $result = !$this->isSilenced();
1083                 break;
1084             case Right::PUBLICNOTICE:
1085             case Right::EMAILONREPLY:
1086             case Right::EMAILONSUBSCRIBE:
1087             case Right::EMAILONFAVE:
1088                 $result = !$this->isSandboxed();
1089                 break;
1090             case Right::WEBLOGIN:
1091                 $result = !$this->isSilenced();
1092                 break;
1093             case Right::API:
1094                 $result = !$this->isSilenced();
1095                 break;
1096             case Right::BACKUPACCOUNT:
1097                 $result = common_config('profile', 'backup');
1098                 break;
1099             case Right::RESTOREACCOUNT:
1100                 $result = common_config('profile', 'restore');
1101                 break;
1102             case Right::DELETEACCOUNT:
1103                 $result = common_config('profile', 'delete');
1104                 break;
1105             case Right::MOVEACCOUNT:
1106                 $result = common_config('profile', 'move');
1107                 break;
1108             default:
1109                 $result = false;
1110                 break;
1111             }
1112         }
1113         return $result;
1114     }
1115
1116     function hasRepeated($notice_id)
1117     {
1118         // XXX: not really a pkey, but should work
1119
1120         $notice = Memcached_DataObject::pkeyGet('Notice',
1121                                                 array('profile_id' => $this->id,
1122                                                       'repeat_of' => $notice_id));
1123
1124         return !empty($notice);
1125     }
1126
1127     /**
1128      * Returns an XML string fragment with limited profile information
1129      * as an Atom <author> element.
1130      *
1131      * Assumes that Atom has been previously set up as the base namespace.
1132      *
1133      * @param Profile $cur the current authenticated user
1134      *
1135      * @return string
1136      */
1137     function asAtomAuthor($cur = null)
1138     {
1139         $xs = new XMLStringer(true);
1140
1141         $xs->elementStart('author');
1142         $xs->element('name', null, $this->nickname);
1143         $xs->element('uri', null, $this->getUri());
1144         if ($cur != null) {
1145             $attrs = Array();
1146             $attrs['following'] = $cur->isSubscribed($this) ? 'true' : 'false';
1147             $attrs['blocking']  = $cur->hasBlocked($this) ? 'true' : 'false';
1148             $xs->element('statusnet:profile_info', $attrs, null);
1149         }
1150         $xs->elementEnd('author');
1151
1152         return $xs->getString();
1153     }
1154
1155     /**
1156      * Extra profile info for atom entries
1157      *
1158      * Clients use some extra profile info in the atom stream.
1159      * This gives it to them.
1160      *
1161      * @param User $cur Current user
1162      *
1163      * @return array representation of <statusnet:profile_info> element or null
1164      */
1165
1166     function profileInfo($cur)
1167     {
1168         $profileInfoAttr = array('local_id' => $this->id);
1169
1170         if ($cur != null) {
1171             // Whether the current user is a subscribed to this profile
1172             $profileInfoAttr['following'] = $cur->isSubscribed($this) ? 'true' : 'false';
1173             // Whether the current user is has blocked this profile
1174             $profileInfoAttr['blocking']  = $cur->hasBlocked($this) ? 'true' : 'false';
1175         }
1176
1177         return array('statusnet:profile_info', $profileInfoAttr, null);
1178     }
1179
1180     /**
1181      * Returns an XML string fragment with profile information as an
1182      * Activity Streams <activity:actor> element.
1183      *
1184      * Assumes that 'activity' namespace has been previously defined.
1185      *
1186      * @return string
1187      */
1188     function asActivityActor()
1189     {
1190         return $this->asActivityNoun('actor');
1191     }
1192
1193     /**
1194      * Returns an XML string fragment with profile information as an
1195      * Activity Streams noun object with the given element type.
1196      *
1197      * Assumes that 'activity', 'georss', and 'poco' namespace has been
1198      * previously defined.
1199      *
1200      * @param string $element one of 'actor', 'subject', 'object', 'target'
1201      *
1202      * @return string
1203      */
1204     function asActivityNoun($element)
1205     {
1206         $noun = ActivityObject::fromProfile($this);
1207         return $noun->asString('activity:' . $element);
1208     }
1209
1210     /**
1211      * Returns the best URI for a profile. Plugins may override.
1212      *
1213      * @return string $uri
1214      */
1215     function getUri()
1216     {
1217         $uri = null;
1218
1219         // give plugins a chance to set the URI
1220         if (Event::handle('StartGetProfileUri', array($this, &$uri))) {
1221
1222             // check for a local user first
1223             $user = User::staticGet('id', $this->id);
1224
1225             if (!empty($user)) {
1226                 $uri = $user->uri;
1227             } else {
1228                 // return OMB profile if any
1229                 $remote = Remote_profile::staticGet('id', $this->id);
1230                 if (!empty($remote)) {
1231                     $uri = $remote->uri;
1232                 }
1233             }
1234             Event::handle('EndGetProfileUri', array($this, &$uri));
1235         }
1236
1237         return $uri;
1238     }
1239
1240     function hasBlocked($other)
1241     {
1242         $block = Profile_block::get($this->id, $other->id);
1243
1244         if (empty($block)) {
1245             $result = false;
1246         } else {
1247             $result = true;
1248         }
1249
1250         return $result;
1251     }
1252
1253     function getAtomFeed()
1254     {
1255         $feed = null;
1256
1257         if (Event::handle('StartProfileGetAtomFeed', array($this, &$feed))) {
1258             $user = User::staticGet('id', $this->id);
1259             if (!empty($user)) {
1260                 $feed = common_local_url('ApiTimelineUser', array('id' => $user->id,
1261                                                                   'format' => 'atom'));
1262             }
1263             Event::handle('EndProfileGetAtomFeed', array($this, $feed));
1264         }
1265
1266         return $feed;
1267     }
1268
1269     static function fromURI($uri)
1270     {
1271         $profile = null;
1272
1273         if (Event::handle('StartGetProfileFromURI', array($uri, &$profile))) {
1274             // Get a local user or remote (OMB 0.1) profile
1275             $user = User::staticGet('uri', $uri);
1276             if (!empty($user)) {
1277                 $profile = $user->getProfile();
1278             } else {
1279                 $remote_profile = Remote_profile::staticGet('uri', $uri);
1280                 if (!empty($remote_profile)) {
1281                     $profile = Profile::staticGet('id', $remote_profile->profile_id);
1282                 }
1283             }
1284             Event::handle('EndGetProfileFromURI', array($uri, $profile));
1285         }
1286
1287         return $profile;
1288     }
1289
1290     function canRead(Notice $notice)
1291     {
1292         if ($notice->scope & Notice::SITE_SCOPE) {
1293             $user = $this->getUser();
1294             if (empty($user)) {
1295                 return false;
1296             }
1297         }
1298
1299         if ($notice->scope & Notice::ADDRESSEE_SCOPE) {
1300             $replies = $notice->getReplies();
1301
1302             if (!in_array($this->id, $replies)) {
1303                 $groups = $notice->getGroups();
1304
1305                 $foundOne = false;
1306
1307                 foreach ($groups as $group) {
1308                     if ($this->isMember($group)) {
1309                         $foundOne = true;
1310                         break;
1311                     }
1312                 }
1313
1314                 if (!$foundOne) {
1315                     return false;
1316                 }
1317             }
1318         }
1319
1320         if ($notice->scope & Notice::FOLLOWER_SCOPE) {
1321             $author = $notice->getProfile();
1322             if (!Subscription::exists($this, $author)) {
1323                 return false;
1324             }
1325         }
1326
1327         return true;
1328     }
1329 }