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