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