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