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