]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - classes/Profile.php
d84d5da290dee37111a7c15fefb250079006b2dd
[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 followed by nickname in parens
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
184     function getCurrentNotice()
185     {
186         $notice = $this->getNotices(0, 1);
187
188         if ($notice->fetch()) {
189             if ($notice instanceof ArrayWrapper) {
190                 // hack for things trying to work with single notices
191                 return $notice->_items[0];
192             }
193             return $notice;
194         } else {
195             return null;
196         }
197     }
198
199     function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
200     {
201         $ids = Notice::stream(array($this, '_streamTaggedDirect'),
202                               array($tag),
203                               'profile:notice_ids_tagged:' . $this->id . ':' . $tag,
204                               $offset, $limit, $since_id, $max_id);
205         return Notice::getStreamByIds($ids);
206     }
207
208     function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
209     {
210         // XXX: I'm not sure this is going to be any faster. It probably isn't.
211         $ids = Notice::stream(array($this, '_streamDirect'),
212                               array(),
213                               'profile:notice_ids:' . $this->id,
214                               $offset, $limit, $since_id, $max_id);
215
216         return Notice::getStreamByIds($ids);
217     }
218
219     function _streamTaggedDirect($tag, $offset, $limit, $since_id, $max_id)
220     {
221         // XXX It would be nice to do this without a join
222         // (necessary to do it efficiently on accounts with long history)
223
224         $notice = new Notice();
225
226         $query =
227           "select id from notice join notice_tag on id=notice_id where tag='".
228           $notice->escape($tag) .
229           "' and profile_id=" . intval($this->id);
230
231         $since = Notice::whereSinceId($since_id, 'id', 'notice.created');
232         if ($since) {
233             $query .= " and ($since)";
234         }
235
236         $max = Notice::whereMaxId($max_id, 'id', 'notice.created');
237         if ($max) {
238             $query .= " and ($max)";
239         }
240
241         $query .= ' order by notice.created DESC, id DESC';
242
243         if (!is_null($offset)) {
244             $query .= " LIMIT " . intval($limit) . " OFFSET " . intval($offset);
245         }
246
247         $notice->query($query);
248
249         $ids = array();
250
251         while ($notice->fetch()) {
252             $ids[] = $notice->id;
253         }
254
255         return $ids;
256     }
257
258     function _streamDirect($offset, $limit, $since_id, $max_id)
259     {
260         $notice = new Notice();
261
262         $notice->profile_id = $this->id;
263
264         $notice->selectAdd();
265         $notice->selectAdd('id');
266
267         Notice::addWhereSinceId($notice, $since_id);
268         Notice::addWhereMaxId($notice, $max_id);
269
270         $notice->orderBy('created DESC, id DESC');
271
272         if (!is_null($offset)) {
273             $notice->limit($offset, $limit);
274         }
275
276         $notice->find();
277
278         $ids = array();
279
280         while ($notice->fetch()) {
281             $ids[] = $notice->id;
282         }
283
284         return $ids;
285     }
286
287     function isMember($group)
288     {
289         $mem = new Group_member();
290
291         $mem->group_id = $group->id;
292         $mem->profile_id = $this->id;
293
294         if ($mem->find()) {
295             return true;
296         } else {
297             return false;
298         }
299     }
300
301     function isAdmin($group)
302     {
303         $mem = new Group_member();
304
305         $mem->group_id = $group->id;
306         $mem->profile_id = $this->id;
307         $mem->is_admin = 1;
308
309         if ($mem->find()) {
310             return true;
311         } else {
312             return false;
313         }
314     }
315
316     function isPendingMember($group)
317     {
318         $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id,
319                                                    'group_id' => $group->id));
320         return !empty($request);
321     }
322
323     function getGroups($offset=0, $limit=null)
324     {
325         $qry =
326           'SELECT user_group.* ' .
327           'FROM user_group JOIN group_member '.
328           'ON user_group.id = group_member.group_id ' .
329           'WHERE group_member.profile_id = %d ' .
330           'ORDER BY group_member.created DESC ';
331
332         if ($offset>0 && !is_null($limit)) {
333             if ($offset) {
334                 if (common_config('db','type') == 'pgsql') {
335                     $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
336                 } else {
337                     $qry .= ' LIMIT ' . $offset . ', ' . $limit;
338                 }
339             }
340         }
341
342         $groups = new User_group();
343
344         $cnt = $groups->query(sprintf($qry, $this->id));
345
346         return $groups;
347     }
348
349     /**
350      * Request to join the given group.
351      * May throw exceptions on failure.
352      *
353      * @param User_group $group
354      * @return mixed: Group_member on success, Group_join_queue if pending approval, null on some cancels?
355      */
356     function joinGroup(User_group $group)
357     {
358         $join = null;
359         if ($group->join_policy == User_group::JOIN_POLICY_MODERATE) {
360             $join = Group_join_queue::saveNew($this, $group);
361         } else {
362             if (Event::handle('StartJoinGroup', array($group, $this))) {
363                 $join = Group_member::join($group->id, $this->id);
364                 Event::handle('EndJoinGroup', array($group, $this));
365             }
366         }
367         if ($join) {
368             // Send any applicable notifications...
369             $join->notify();
370         }
371         return $join;
372     }
373
374     /**
375      * Cancel a pending group join...
376      *
377      * @param User_group $group
378      */
379     function cancelJoinGroup(User_group $group)
380     {
381         $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id,
382                                                    'group_id' => $group->id));
383         if ($request) {
384             if (Event::handle('StartCancelJoinGroup', array($group, $this))) {
385                 $request->delete();
386                 Event::handle('EndCancelJoinGroup', array($group, $this));
387             }
388         }
389     }
390
391     /**
392      * Complete a pending group join on our end...
393      *
394      * @param User_group $group
395      */
396     function completeJoinGroup(User_group $group)
397     {
398         $join = null;
399         $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id,
400                                                    'group_id' => $group->id));
401         if ($request) {
402             if (Event::handle('StartJoinGroup', array($group, $this))) {
403                 $join = Group_member::join($group->id, $this->id);
404                 $request->delete();
405                 Event::handle('EndJoinGroup', array($group, $this));
406             }
407         } else {
408             throw new Exception(_m('Invalid group join approval: not pending.'));
409         }
410         if ($join) {
411             $join->notify();
412         }
413         return $join;
414     }
415
416     /**
417      * Leave a group that this profile is a member of.
418      *
419      * @param User_group $group 
420      */
421     function leaveGroup(User_group $group)
422     {
423         if (Event::handle('StartLeaveGroup', array($group, $this))) {
424             Group_member::leave($group->id, $this->id);
425             Event::handle('EndLeaveGroup', array($group, $this));
426         }
427     }
428
429     function avatarUrl($size=AVATAR_PROFILE_SIZE)
430     {
431         $avatar = $this->getAvatar($size);
432         if ($avatar) {
433             return $avatar->displayUrl();
434         } else {
435             return Avatar::defaultImage($size);
436         }
437     }
438
439     function getSubscriptions($offset=0, $limit=null)
440     {
441         $subs = Subscription::bySubscriber($this->id,
442                                            $offset,
443                                            $limit);
444
445         $profiles = array();
446
447         while ($subs->fetch()) {
448             $profile = Profile::staticGet($subs->subscribed);
449             if ($profile) {
450                 $profiles[] = $profile;
451             }
452         }
453
454         return new ArrayWrapper($profiles);
455     }
456
457     function getSubscribers($offset=0, $limit=null)
458     {
459         $subs = Subscription::bySubscribed($this->id,
460                                            $offset,
461                                            $limit);
462
463         $profiles = array();
464
465         while ($subs->fetch()) {
466             $profile = Profile::staticGet($subs->subscriber);
467             if ($profile) {
468                 $profiles[] = $profile;
469             }
470         }
471
472         return new ArrayWrapper($profiles);
473     }
474
475     function subscriptionCount()
476     {
477         $c = Cache::instance();
478
479         if (!empty($c)) {
480             $cnt = $c->get(Cache::key('profile:subscription_count:'.$this->id));
481             if (is_integer($cnt)) {
482                 return (int) $cnt;
483             }
484         }
485
486         $sub = new Subscription();
487         $sub->subscriber = $this->id;
488
489         $cnt = (int) $sub->count('distinct subscribed');
490
491         $cnt = ($cnt > 0) ? $cnt - 1 : $cnt;
492
493         if (!empty($c)) {
494             $c->set(Cache::key('profile:subscription_count:'.$this->id), $cnt);
495         }
496
497         return $cnt;
498     }
499
500     function subscriberCount()
501     {
502         $c = Cache::instance();
503         if (!empty($c)) {
504             $cnt = $c->get(Cache::key('profile:subscriber_count:'.$this->id));
505             if (is_integer($cnt)) {
506                 return (int) $cnt;
507             }
508         }
509
510         $sub = new Subscription();
511         $sub->subscribed = $this->id;
512         $sub->whereAdd('subscriber != subscribed');
513         $cnt = (int) $sub->count('distinct subscriber');
514
515         if (!empty($c)) {
516             $c->set(Cache::key('profile:subscriber_count:'.$this->id), $cnt);
517         }
518
519         return $cnt;
520     }
521
522     /**
523      * Is this profile subscribed to another profile?
524      *
525      * @param Profile $other
526      * @return boolean
527      */
528     function isSubscribed($other)
529     {
530         return Subscription::exists($this, $other);
531     }
532
533     /**
534      * Are these two profiles subscribed to each other?
535      *
536      * @param Profile $other
537      * @return boolean
538      */
539     function mutuallySubscribed($other)
540     {
541         return $this->isSubscribed($other) &&
542           $other->isSubscribed($this);
543     }
544
545     function hasFave($notice)
546     {
547         $cache = Cache::instance();
548
549         // XXX: Kind of a hack.
550
551         if (!empty($cache)) {
552             // This is the stream of favorite notices, in rev chron
553             // order. This forces it into cache.
554
555             $ids = Fave::stream($this->id, 0, NOTICE_CACHE_WINDOW);
556
557             // If it's in the list, then it's a fave
558
559             if (in_array($notice->id, $ids)) {
560                 return true;
561             }
562
563             // If we're not past the end of the cache window,
564             // then the cache has all available faves, so this one
565             // is not a fave.
566
567             if (count($ids) < NOTICE_CACHE_WINDOW) {
568                 return false;
569             }
570
571             // Otherwise, cache doesn't have all faves;
572             // fall through to the default
573         }
574
575         $fave = Fave::pkeyGet(array('user_id' => $this->id,
576                                     'notice_id' => $notice->id));
577         return ((is_null($fave)) ? false : true);
578     }
579
580     function faveCount()
581     {
582         $c = Cache::instance();
583         if (!empty($c)) {
584             $cnt = $c->get(Cache::key('profile:fave_count:'.$this->id));
585             if (is_integer($cnt)) {
586                 return (int) $cnt;
587             }
588         }
589
590         $faves = new Fave();
591         $faves->user_id = $this->id;
592         $cnt = (int) $faves->count('distinct notice_id');
593
594         if (!empty($c)) {
595             $c->set(Cache::key('profile:fave_count:'.$this->id), $cnt);
596         }
597
598         return $cnt;
599     }
600
601     function noticeCount()
602     {
603         $c = Cache::instance();
604
605         if (!empty($c)) {
606             $cnt = $c->get(Cache::key('profile:notice_count:'.$this->id));
607             if (is_integer($cnt)) {
608                 return (int) $cnt;
609             }
610         }
611
612         $notices = new Notice();
613         $notices->profile_id = $this->id;
614         $cnt = (int) $notices->count('distinct id');
615
616         if (!empty($c)) {
617             $c->set(Cache::key('profile:notice_count:'.$this->id), $cnt);
618         }
619
620         return $cnt;
621     }
622
623     function blowFavesCache()
624     {
625         $cache = Cache::instance();
626         if ($cache) {
627             // Faves don't happen chronologically, so we need to blow
628             // ;last cache, too
629             $cache->delete(Cache::key('fave:ids_by_user:'.$this->id));
630             $cache->delete(Cache::key('fave:ids_by_user:'.$this->id.';last'));
631             $cache->delete(Cache::key('fave:ids_by_user_own:'.$this->id));
632             $cache->delete(Cache::key('fave:ids_by_user_own:'.$this->id.';last'));
633         }
634         $this->blowFaveCount();
635     }
636
637     function blowSubscriberCount()
638     {
639         $c = Cache::instance();
640         if (!empty($c)) {
641             $c->delete(Cache::key('profile:subscriber_count:'.$this->id));
642         }
643     }
644
645     function blowSubscriptionCount()
646     {
647         $c = Cache::instance();
648         if (!empty($c)) {
649             $c->delete(Cache::key('profile:subscription_count:'.$this->id));
650         }
651     }
652
653     function blowFaveCount()
654     {
655         $c = Cache::instance();
656         if (!empty($c)) {
657             $c->delete(Cache::key('profile:fave_count:'.$this->id));
658         }
659     }
660
661     function blowNoticeCount()
662     {
663         $c = Cache::instance();
664         if (!empty($c)) {
665             $c->delete(Cache::key('profile:notice_count:'.$this->id));
666         }
667     }
668
669     static function maxBio()
670     {
671         $biolimit = common_config('profile', 'biolimit');
672         // null => use global limit (distinct from 0!)
673         if (is_null($biolimit)) {
674             $biolimit = common_config('site', 'textlimit');
675         }
676         return $biolimit;
677     }
678
679     static function bioTooLong($bio)
680     {
681         $biolimit = self::maxBio();
682         return ($biolimit > 0 && !empty($bio) && (mb_strlen($bio) > $biolimit));
683     }
684
685     function delete()
686     {
687         $this->_deleteNotices();
688         $this->_deleteSubscriptions();
689         $this->_deleteMessages();
690         $this->_deleteTags();
691         $this->_deleteBlocks();
692         $this->delete_avatars();
693
694         // Warning: delete() will run on the batch objects,
695         // not on individual objects.
696         $related = array('Reply',
697                          'Group_member',
698                          );
699         Event::handle('ProfileDeleteRelated', array($this, &$related));
700
701         foreach ($related as $cls) {
702             $inst = new $cls();
703             $inst->profile_id = $this->id;
704             $inst->delete();
705         }
706
707         parent::delete();
708     }
709
710     function _deleteNotices()
711     {
712         $notice = new Notice();
713         $notice->profile_id = $this->id;
714
715         if ($notice->find()) {
716             while ($notice->fetch()) {
717                 $other = clone($notice);
718                 $other->delete();
719             }
720         }
721     }
722
723     function _deleteSubscriptions()
724     {
725         $sub = new Subscription();
726         $sub->subscriber = $this->id;
727
728         $sub->find();
729
730         while ($sub->fetch()) {
731             $other = Profile::staticGet('id', $sub->subscribed);
732             if (empty($other)) {
733                 continue;
734             }
735             if ($other->id == $this->id) {
736                 continue;
737             }
738             Subscription::cancel($this, $other);
739         }
740
741         $subd = new Subscription();
742         $subd->subscribed = $this->id;
743         $subd->find();
744
745         while ($subd->fetch()) {
746             $other = Profile::staticGet('id', $subd->subscriber);
747             if (empty($other)) {
748                 continue;
749             }
750             if ($other->id == $this->id) {
751                 continue;
752             }
753             Subscription::cancel($other, $this);
754         }
755
756         $self = new Subscription();
757
758         $self->subscriber = $this->id;
759         $self->subscribed = $this->id;
760
761         $self->delete();
762     }
763
764     function _deleteMessages()
765     {
766         $msg = new Message();
767         $msg->from_profile = $this->id;
768         $msg->delete();
769
770         $msg = new Message();
771         $msg->to_profile = $this->id;
772         $msg->delete();
773     }
774
775     function _deleteTags()
776     {
777         $tag = new Profile_tag();
778         $tag->tagged = $this->id;
779         $tag->delete();
780     }
781
782     function _deleteBlocks()
783     {
784         $block = new Profile_block();
785         $block->blocked = $this->id;
786         $block->delete();
787
788         $block = new Group_block();
789         $block->blocked = $this->id;
790         $block->delete();
791     }
792
793     // XXX: identical to Notice::getLocation.
794
795     function getLocation()
796     {
797         $location = null;
798
799         if (!empty($this->location_id) && !empty($this->location_ns)) {
800             $location = Location::fromId($this->location_id, $this->location_ns);
801         }
802
803         if (is_null($location)) { // no ID, or Location::fromId() failed
804             if (!empty($this->lat) && !empty($this->lon)) {
805                 $location = Location::fromLatLon($this->lat, $this->lon);
806             }
807         }
808
809         if (is_null($location)) { // still haven't found it!
810             if (!empty($this->location)) {
811                 $location = Location::fromName($this->location);
812             }
813         }
814
815         return $location;
816     }
817
818     function hasRole($name)
819     {
820         $has_role = false;
821         if (Event::handle('StartHasRole', array($this, $name, &$has_role))) {
822             $role = Profile_role::pkeyGet(array('profile_id' => $this->id,
823                                                 'role' => $name));
824             $has_role = !empty($role);
825             Event::handle('EndHasRole', array($this, $name, $has_role));
826         }
827         return $has_role;
828     }
829
830     function grantRole($name)
831     {
832         if (Event::handle('StartGrantRole', array($this, $name))) {
833
834             $role = new Profile_role();
835
836             $role->profile_id = $this->id;
837             $role->role       = $name;
838             $role->created    = common_sql_now();
839
840             $result = $role->insert();
841
842             if (!$result) {
843                 throw new Exception("Can't save role '$name' for profile '{$this->id}'");
844             }
845
846             if ($name == 'owner') {
847                 User::blow('user:site_owner');
848             }
849
850             Event::handle('EndGrantRole', array($this, $name));
851         }
852
853         return $result;
854     }
855
856     function revokeRole($name)
857     {
858         if (Event::handle('StartRevokeRole', array($this, $name))) {
859
860             $role = Profile_role::pkeyGet(array('profile_id' => $this->id,
861                                                 'role' => $name));
862
863             if (empty($role)) {
864                 // TRANS: Exception thrown when trying to revoke an existing role for a user that does not exist.
865                 // TRANS: %1$s is the role name, %2$s is the user ID (number).
866                 throw new Exception(sprintf(_('Cannot revoke role "%1$s" for user #%2$d; does not exist.'),$name, $this->id));
867             }
868
869             $result = $role->delete();
870
871             if (!$result) {
872                 common_log_db_error($role, 'DELETE', __FILE__);
873                 // TRANS: Exception thrown when trying to revoke a role for a user with a failing database query.
874                 // TRANS: %1$s is the role name, %2$s is the user ID (number).
875                 throw new Exception(sprintf(_('Cannot revoke role "%1$s" for user #%2$d; database error.'),$name, $this->id));
876             }
877
878             if ($name == 'owner') {
879                 User::blow('user:site_owner');
880             }
881
882             Event::handle('EndRevokeRole', array($this, $name));
883
884             return true;
885         }
886     }
887
888     function isSandboxed()
889     {
890         return $this->hasRole(Profile_role::SANDBOXED);
891     }
892
893     function isSilenced()
894     {
895         return $this->hasRole(Profile_role::SILENCED);
896     }
897
898     function sandbox()
899     {
900         $this->grantRole(Profile_role::SANDBOXED);
901     }
902
903     function unsandbox()
904     {
905         $this->revokeRole(Profile_role::SANDBOXED);
906     }
907
908     function silence()
909     {
910         $this->grantRole(Profile_role::SILENCED);
911     }
912
913     function unsilence()
914     {
915         $this->revokeRole(Profile_role::SILENCED);
916     }
917
918     /**
919      * Does this user have the right to do X?
920      *
921      * With our role-based authorization, this is merely a lookup for whether the user
922      * has a particular role. The implementation currently uses a switch statement
923      * to determine if the user has the pre-defined role to exercise the right. Future
924      * implementations may allow per-site roles, and different mappings of roles to rights.
925      *
926      * @param $right string Name of the right, usually a constant in class Right
927      * @return boolean whether the user has the right in question
928      */
929     function hasRight($right)
930     {
931         $result = false;
932
933         if ($this->hasRole(Profile_role::DELETED)) {
934             return false;
935         }
936
937         if (Event::handle('UserRightsCheck', array($this, $right, &$result))) {
938             switch ($right)
939             {
940             case Right::DELETEOTHERSNOTICE:
941             case Right::MAKEGROUPADMIN:
942             case Right::SANDBOXUSER:
943             case Right::SILENCEUSER:
944             case Right::DELETEUSER:
945             case Right::DELETEGROUP:
946                 $result = $this->hasRole(Profile_role::MODERATOR);
947                 break;
948             case Right::CONFIGURESITE:
949                 $result = $this->hasRole(Profile_role::ADMINISTRATOR);
950                 break;
951             case Right::GRANTROLE:
952             case Right::REVOKEROLE:
953                 $result = $this->hasRole(Profile_role::OWNER);
954                 break;
955             case Right::NEWNOTICE:
956             case Right::NEWMESSAGE:
957             case Right::SUBSCRIBE:
958             case Right::CREATEGROUP:
959                 $result = !$this->isSilenced();
960                 break;
961             case Right::PUBLICNOTICE:
962             case Right::EMAILONREPLY:
963             case Right::EMAILONSUBSCRIBE:
964             case Right::EMAILONFAVE:
965                 $result = !$this->isSandboxed();
966                 break;
967             case Right::WEBLOGIN:
968                 $result = !$this->isSilenced();
969                 break;
970             case Right::API:
971                 $result = !$this->isSilenced();
972                 break;
973             case Right::BACKUPACCOUNT:
974                 $result = common_config('profile', 'backup');
975                 break;
976             case Right::RESTOREACCOUNT:
977                 $result = common_config('profile', 'restore');
978                 break;
979             case Right::DELETEACCOUNT:
980                 $result = common_config('profile', 'delete');
981                 break;
982             case Right::MOVEACCOUNT:
983                 $result = common_config('profile', 'move');
984                 break;
985             default:
986                 $result = false;
987                 break;
988             }
989         }
990         return $result;
991     }
992
993     function hasRepeated($notice_id)
994     {
995         // XXX: not really a pkey, but should work
996
997         $notice = Memcached_DataObject::pkeyGet('Notice',
998                                                 array('profile_id' => $this->id,
999                                                       'repeat_of' => $notice_id));
1000
1001         return !empty($notice);
1002     }
1003
1004     /**
1005      * Returns an XML string fragment with limited profile information
1006      * as an Atom <author> element.
1007      *
1008      * Assumes that Atom has been previously set up as the base namespace.
1009      *
1010      * @param Profile $cur the current authenticated user
1011      *
1012      * @return string
1013      */
1014     function asAtomAuthor($cur = null)
1015     {
1016         $xs = new XMLStringer(true);
1017
1018         $xs->elementStart('author');
1019         $xs->element('name', null, $this->nickname);
1020         $xs->element('uri', null, $this->getUri());
1021         if ($cur != null) {
1022             $attrs = Array();
1023             $attrs['following'] = $cur->isSubscribed($this) ? 'true' : 'false';
1024             $attrs['blocking']  = $cur->hasBlocked($this) ? 'true' : 'false';
1025             $xs->element('statusnet:profile_info', $attrs, null);
1026         }
1027         $xs->elementEnd('author');
1028
1029         return $xs->getString();
1030     }
1031
1032     /**
1033      * Extra profile info for atom entries
1034      *
1035      * Clients use some extra profile info in the atom stream.
1036      * This gives it to them.
1037      *
1038      * @param User $cur Current user
1039      *
1040      * @return array representation of <statusnet:profile_info> element or null
1041      */
1042
1043     function profileInfo($cur)
1044     {
1045         $profileInfoAttr = array('local_id' => $this->id);
1046
1047         if ($cur != null) {
1048             // Whether the current user is a subscribed to this profile
1049             $profileInfoAttr['following'] = $cur->isSubscribed($this) ? 'true' : 'false';
1050             // Whether the current user is has blocked this profile
1051             $profileInfoAttr['blocking']  = $cur->hasBlocked($this) ? 'true' : 'false';
1052         }
1053
1054         return array('statusnet:profile_info', $profileInfoAttr, null);
1055     }
1056
1057     /**
1058      * Returns an XML string fragment with profile information as an
1059      * Activity Streams <activity:actor> element.
1060      *
1061      * Assumes that 'activity' namespace has been previously defined.
1062      *
1063      * @return string
1064      */
1065     function asActivityActor()
1066     {
1067         return $this->asActivityNoun('actor');
1068     }
1069
1070     /**
1071      * Returns an XML string fragment with profile information as an
1072      * Activity Streams noun object with the given element type.
1073      *
1074      * Assumes that 'activity', 'georss', and 'poco' namespace has been
1075      * previously defined.
1076      *
1077      * @param string $element one of 'actor', 'subject', 'object', 'target'
1078      *
1079      * @return string
1080      */
1081     function asActivityNoun($element)
1082     {
1083         $noun = ActivityObject::fromProfile($this);
1084         return $noun->asString('activity:' . $element);
1085     }
1086
1087     /**
1088      * Returns the best URI for a profile. Plugins may override.
1089      *
1090      * @return string $uri
1091      */
1092     function getUri()
1093     {
1094         $uri = null;
1095
1096         // give plugins a chance to set the URI
1097         if (Event::handle('StartGetProfileUri', array($this, &$uri))) {
1098
1099             // check for a local user first
1100             $user = User::staticGet('id', $this->id);
1101
1102             if (!empty($user)) {
1103                 $uri = $user->uri;
1104             } else {
1105                 // return OMB profile if any
1106                 $remote = Remote_profile::staticGet('id', $this->id);
1107                 if (!empty($remote)) {
1108                     $uri = $remote->uri;
1109                 }
1110             }
1111             Event::handle('EndGetProfileUri', array($this, &$uri));
1112         }
1113
1114         return $uri;
1115     }
1116
1117     function hasBlocked($other)
1118     {
1119         $block = Profile_block::get($this->id, $other->id);
1120
1121         if (empty($block)) {
1122             $result = false;
1123         } else {
1124             $result = true;
1125         }
1126
1127         return $result;
1128     }
1129
1130     function getAtomFeed()
1131     {
1132         $feed = null;
1133
1134         if (Event::handle('StartProfileGetAtomFeed', array($this, &$feed))) {
1135             $user = User::staticGet('id', $this->id);
1136             if (!empty($user)) {
1137                 $feed = common_local_url('ApiTimelineUser', array('id' => $user->id,
1138                                                                   'format' => 'atom'));
1139             }
1140             Event::handle('EndProfileGetAtomFeed', array($this, $feed));
1141         }
1142
1143         return $feed;
1144     }
1145
1146     static function fromURI($uri)
1147     {
1148         $profile = null;
1149
1150         if (Event::handle('StartGetProfileFromURI', array($uri, &$profile))) {
1151             // Get a local user or remote (OMB 0.1) profile
1152             $user = User::staticGet('uri', $uri);
1153             if (!empty($user)) {
1154                 $profile = $user->getProfile();
1155             } else {
1156                 $remote_profile = Remote_profile::staticGet('uri', $uri);
1157                 if (!empty($remote_profile)) {
1158                     $profile = Profile::staticGet('id', $remote_profile->profile_id);
1159                 }
1160             }
1161             Event::handle('EndGetProfileFromURI', array($uri, $profile));
1162         }
1163
1164         return $profile;
1165     }
1166 }