common_memcache() => Cache::instance()
[statusnet-biz:statusnet-biz.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
107                 $scaled_filename = $imagefile->resize($size);
108
109                 //$scaled = DB_DataObject::factory('avatar');
110                 $scaled = new Avatar();
111                 $scaled->profile_id = $this->id;
112                 $scaled->width = $size;
113                 $scaled->height = $size;
114                 $scaled->original = false;
115                 $scaled->mediatype = image_type_to_mime_type($imagefile->type);
116                 $scaled->filename = $scaled_filename;
117                 $scaled->url = Avatar::url($scaled_filename);
118                 $scaled->created = DB_DataObject_Cast::dateTime(); # current time
119
120                 if (!$scaled->insert()) {
121                     return null;
122                 }
123             }
124         }
125
126         return $avatar;
127     }
128
129     function delete_avatars($original=true)
130     {
131         $avatar = new Avatar();
132         $avatar->profile_id = $this->id;
133         $avatar->find();
134         while ($avatar->fetch()) {
135             if ($avatar->original) {
136                 if ($original == false) {
137                     continue;
138                 }
139             }
140             $avatar->delete();
141         }
142         return true;
143     }
144
145     function getBestName()
146     {
147         return ($this->fullname) ? $this->fullname : $this->nickname;
148     }
149
150     /**
151      * Get the most recent notice posted by this user, if any.
152      *
153      * @return mixed Notice or null
154      */
155
156     function getCurrentNotice()
157     {
158         $notice = $this->getNotices(0, 1);
159
160         if ($notice->fetch()) {
161             return $notice;
162         } else {
163             return null;
164         }
165     }
166
167     function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
168     {
169         $ids = Notice::stream(array($this, '_streamTaggedDirect'),
170                               array($tag),
171                               'profile:notice_ids_tagged:' . $this->id . ':' . $tag,
172                               $offset, $limit, $since_id, $max_id);
173         return Notice::getStreamByIds($ids);
174     }
175
176     function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
177     {
178         // XXX: I'm not sure this is going to be any faster. It probably isn't.
179         $ids = Notice::stream(array($this, '_streamDirect'),
180                               array(),
181                               'profile:notice_ids:' . $this->id,
182                               $offset, $limit, $since_id, $max_id);
183
184         return Notice::getStreamByIds($ids);
185     }
186
187     function _streamTaggedDirect($tag, $offset, $limit, $since_id, $max_id)
188     {
189         // XXX It would be nice to do this without a join
190
191         $notice = new Notice();
192
193         $query =
194           "select id from notice join notice_tag on id=notice_id where tag='".
195           $notice->escape($tag) .
196           "' and profile_id=" . $notice->escape($this->id);
197
198         if ($since_id != 0) {
199             $query .= " and id > $since_id";
200         }
201
202         if ($max_id != 0) {
203             $query .= " and id < $max_id";
204         }
205
206         $query .= ' order by id DESC';
207
208         if (!is_null($offset)) {
209             $query .= " LIMIT $limit OFFSET $offset";
210         }
211
212         $notice->query($query);
213
214         $ids = array();
215
216         while ($notice->fetch()) {
217             $ids[] = $notice->id;
218         }
219
220         return $ids;
221     }
222
223     function _streamDirect($offset, $limit, $since_id, $max_id)
224     {
225         $notice = new Notice();
226
227         // Temporary hack until notice_profile_id_idx is updated
228         // to (profile_id, id) instead of (profile_id, created, id).
229         // It's been falling back to PRIMARY instead, which is really
230         // very inefficient for a profile that hasn't posted in a few
231         // months. Even though forcing the index will cause a filesort,
232         // it's usually going to be better.
233         if (common_config('db', 'type') == 'mysql') {
234             $index = '';
235             $query =
236               "select id from notice force index (notice_profile_id_idx) ".
237               "where profile_id=" . $notice->escape($this->id);
238
239             if ($since_id != 0) {
240                 $query .= " and id > $since_id";
241             }
242
243             if ($max_id != 0) {
244                 $query .= " and id < $max_id";
245             }
246
247             $query .= ' order by id DESC';
248
249             if (!is_null($offset)) {
250                 $query .= " LIMIT $limit OFFSET $offset";
251             }
252
253             $notice->query($query);
254         } else {
255             $index = '';
256
257             $notice->profile_id = $this->id;
258
259             $notice->selectAdd();
260             $notice->selectAdd('id');
261
262             if ($since_id != 0) {
263                 $notice->whereAdd('id > ' . $since_id);
264             }
265
266             if ($max_id != 0) {
267                 $notice->whereAdd('id <= ' . $max_id);
268             }
269
270             $notice->orderBy('id DESC');
271
272             if (!is_null($offset)) {
273                 $notice->limit($offset, $limit);
274             }
275
276             $notice->find();
277         }
278
279         $ids = array();
280
281         while ($notice->fetch()) {
282             $ids[] = $notice->id;
283         }
284
285         return $ids;
286     }
287
288     function isMember($group)
289     {
290         $mem = new Group_member();
291
292         $mem->group_id = $group->id;
293         $mem->profile_id = $this->id;
294
295         if ($mem->find()) {
296             return true;
297         } else {
298             return false;
299         }
300     }
301
302     function isAdmin($group)
303     {
304         $mem = new Group_member();
305
306         $mem->group_id = $group->id;
307         $mem->profile_id = $this->id;
308         $mem->is_admin = 1;
309
310         if ($mem->find()) {
311             return true;
312         } else {
313             return false;
314         }
315     }
316
317     function getGroups($offset=0, $limit=null)
318     {
319         $qry =
320           'SELECT user_group.* ' .
321           'FROM user_group JOIN group_member '.
322           'ON user_group.id = group_member.group_id ' .
323           'WHERE group_member.profile_id = %d ' .
324           'ORDER BY group_member.created DESC ';
325
326         if ($offset>0 && !is_null($limit)) {
327             if ($offset) {
328                 if (common_config('db','type') == 'pgsql') {
329                     $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
330                 } else {
331                     $qry .= ' LIMIT ' . $offset . ', ' . $limit;
332                 }
333             }
334         }
335
336         $groups = new User_group();
337
338         $cnt = $groups->query(sprintf($qry, $this->id));
339
340         return $groups;
341     }
342
343     function avatarUrl($size=AVATAR_PROFILE_SIZE)
344     {
345         $avatar = $this->getAvatar($size);
346         if ($avatar) {
347             return $avatar->displayUrl();
348         } else {
349             return Avatar::defaultImage($size);
350         }
351     }
352
353     function getSubscriptions($offset=0, $limit=null)
354     {
355         $qry =
356           'SELECT profile.* ' .
357           'FROM profile JOIN subscription ' .
358           'ON profile.id = subscription.subscribed ' .
359           'WHERE subscription.subscriber = %d ' .
360           'AND subscription.subscribed != subscription.subscriber ' .
361           'ORDER BY subscription.created DESC ';
362
363         if ($offset>0 && !is_null($limit)){
364             if (common_config('db','type') == 'pgsql') {
365                 $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
366             } else {
367                 $qry .= ' LIMIT ' . $offset . ', ' . $limit;
368             }
369         }
370
371         $profile = new Profile();
372
373         $profile->query(sprintf($qry, $this->id));
374
375         return $profile;
376     }
377
378     function getSubscribers($offset=0, $limit=null)
379     {
380         $qry =
381           'SELECT profile.* ' .
382           'FROM profile JOIN subscription ' .
383           'ON profile.id = subscription.subscriber ' .
384           'WHERE subscription.subscribed = %d ' .
385           'AND subscription.subscribed != subscription.subscriber ' .
386           'ORDER BY subscription.created DESC ';
387
388         if ($offset>0 && !is_null($limit)){
389             if ($offset) {
390                 if (common_config('db','type') == 'pgsql') {
391                     $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
392                 } else {
393                     $qry .= ' LIMIT ' . $offset . ', ' . $limit;
394                 }
395             }
396         }
397
398         $profile = new Profile();
399
400         $cnt = $profile->query(sprintf($qry, $this->id));
401
402         return $profile;
403     }
404
405     function getApplications($offset = 0, $limit = null)
406     {
407         $qry =
408           'SELECT a.* ' .
409           'FROM oauth_application_user u, oauth_application a ' .
410           'WHERE u.profile_id = %d ' .
411           'AND a.id = u.application_id ' .
412           'AND u.access_type > 0 ' .
413           'ORDER BY u.created DESC ';
414
415         if ($offset > 0) {
416             if (common_config('db','type') == 'pgsql') {
417                 $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
418             } else {
419                 $qry .= ' LIMIT ' . $offset . ', ' . $limit;
420             }
421         }
422
423         $application = new Oauth_application();
424
425         $cnt = $application->query(sprintf($qry, $this->id));
426
427         return $application;
428     }
429
430     function subscriptionCount()
431     {
432         $c = Cache::instance();
433
434         if (!empty($c)) {
435             $cnt = $c->get(common_cache_key('profile:subscription_count:'.$this->id));
436             if (is_integer($cnt)) {
437                 return (int) $cnt;
438             }
439         }
440
441         $sub = new Subscription();
442         $sub->subscriber = $this->id;
443
444         $cnt = (int) $sub->count('distinct subscribed');
445
446         $cnt = ($cnt > 0) ? $cnt - 1 : $cnt;
447
448         if (!empty($c)) {
449             $c->set(common_cache_key('profile:subscription_count:'.$this->id), $cnt);
450         }
451
452         return $cnt;
453     }
454
455     function subscriberCount()
456     {
457         $c = Cache::instance();
458         if (!empty($c)) {
459             $cnt = $c->get(common_cache_key('profile:subscriber_count:'.$this->id));
460             if (is_integer($cnt)) {
461                 return (int) $cnt;
462             }
463         }
464
465         $sub = new Subscription();
466         $sub->subscribed = $this->id;
467         $sub->whereAdd('subscriber != subscribed');
468         $cnt = (int) $sub->count('distinct subscriber');
469
470         if (!empty($c)) {
471             $c->set(common_cache_key('profile:subscriber_count:'.$this->id), $cnt);
472         }
473
474         return $cnt;
475     }
476
477     function faveCount()
478     {
479         $c = Cache::instance();
480         if (!empty($c)) {
481             $cnt = $c->get(common_cache_key('profile:fave_count:'.$this->id));
482             if (is_integer($cnt)) {
483                 return (int) $cnt;
484             }
485         }
486
487         $faves = new Fave();
488         $faves->user_id = $this->id;
489         $cnt = (int) $faves->count('distinct notice_id');
490
491         if (!empty($c)) {
492             $c->set(common_cache_key('profile:fave_count:'.$this->id), $cnt);
493         }
494
495         return $cnt;
496     }
497
498     function noticeCount()
499     {
500         $c = Cache::instance();
501
502         if (!empty($c)) {
503             $cnt = $c->get(common_cache_key('profile:notice_count:'.$this->id));
504             if (is_integer($cnt)) {
505                 return (int) $cnt;
506             }
507         }
508
509         $notices = new Notice();
510         $notices->profile_id = $this->id;
511         $cnt = (int) $notices->count('distinct id');
512
513         if (!empty($c)) {
514             $c->set(common_cache_key('profile:notice_count:'.$this->id), $cnt);
515         }
516
517         return $cnt;
518     }
519
520     function blowSubscriberCount()
521     {
522         $c = Cache::instance();
523         if (!empty($c)) {
524             $c->delete(common_cache_key('profile:subscriber_count:'.$this->id));
525         }
526     }
527
528     function blowSubscriptionCount()
529     {
530         $c = Cache::instance();
531         if (!empty($c)) {
532             $c->delete(common_cache_key('profile:subscription_count:'.$this->id));
533         }
534     }
535
536     function blowFaveCount()
537     {
538         $c = Cache::instance();
539         if (!empty($c)) {
540             $c->delete(common_cache_key('profile:fave_count:'.$this->id));
541         }
542     }
543
544     function blowNoticeCount()
545     {
546         $c = Cache::instance();
547         if (!empty($c)) {
548             $c->delete(common_cache_key('profile:notice_count:'.$this->id));
549         }
550     }
551
552     static function maxBio()
553     {
554         $biolimit = common_config('profile', 'biolimit');
555         // null => use global limit (distinct from 0!)
556         if (is_null($biolimit)) {
557             $biolimit = common_config('site', 'textlimit');
558         }
559         return $biolimit;
560     }
561
562     static function bioTooLong($bio)
563     {
564         $biolimit = self::maxBio();
565         return ($biolimit > 0 && !empty($bio) && (mb_strlen($bio) > $biolimit));
566     }
567
568     function delete()
569     {
570         $this->_deleteNotices();
571         $this->_deleteSubscriptions();
572         $this->_deleteMessages();
573         $this->_deleteTags();
574         $this->_deleteBlocks();
575
576         $related = array('Avatar',
577                          'Reply',
578                          'Group_member',
579                          );
580         Event::handle('ProfileDeleteRelated', array($this, &$related));
581
582         foreach ($related as $cls) {
583             $inst = new $cls();
584             $inst->profile_id = $this->id;
585             $inst->delete();
586         }
587
588         parent::delete();
589     }
590
591     function _deleteNotices()
592     {
593         $notice = new Notice();
594         $notice->profile_id = $this->id;
595
596         if ($notice->find()) {
597             while ($notice->fetch()) {
598                 $other = clone($notice);
599                 $other->delete();
600             }
601         }
602     }
603
604     function _deleteSubscriptions()
605     {
606         $sub = new Subscription();
607         $sub->subscriber = $this->id;
608
609         $sub->find();
610
611         while ($sub->fetch()) {
612             $other = Profile::staticGet('id', $sub->subscribed);
613             if (empty($other)) {
614                 continue;
615             }
616             if ($other->id == $this->id) {
617                 continue;
618             }
619             Subscription::cancel($this, $other);
620         }
621
622         $subd = new Subscription();
623         $subd->subscribed = $this->id;
624         $subd->find();
625
626         while ($subd->fetch()) {
627             $other = Profile::staticGet('id', $subd->subscriber);
628             if (empty($other)) {
629                 continue;
630             }
631             if ($other->id == $this->id) {
632                 continue;
633             }
634             Subscription::cancel($other, $this);
635         }
636
637         $self = new Subscription();
638
639         $self->subscriber = $this->id;
640         $self->subscribed = $this->id;
641
642         $self->delete();
643     }
644
645     function _deleteMessages()
646     {
647         $msg = new Message();
648         $msg->from_profile = $this->id;
649         $msg->delete();
650
651         $msg = new Message();
652         $msg->to_profile = $this->id;
653         $msg->delete();
654     }
655
656     function _deleteTags()
657     {
658         $tag = new Profile_tag();
659         $tag->tagged = $this->id;
660         $tag->delete();
661     }
662
663     function _deleteBlocks()
664     {
665         $block = new Profile_block();
666         $block->blocked = $this->id;
667         $block->delete();
668
669         $block = new Group_block();
670         $block->blocked = $this->id;
671         $block->delete();
672     }
673
674     // XXX: identical to Notice::getLocation.
675
676     function getLocation()
677     {
678         $location = null;
679
680         if (!empty($this->location_id) && !empty($this->location_ns)) {
681             $location = Location::fromId($this->location_id, $this->location_ns);
682         }
683
684         if (is_null($location)) { // no ID, or Location::fromId() failed
685             if (!empty($this->lat) && !empty($this->lon)) {
686                 $location = Location::fromLatLon($this->lat, $this->lon);
687             }
688         }
689
690         if (is_null($location)) { // still haven't found it!
691             if (!empty($this->location)) {
692                 $location = Location::fromName($this->location);
693             }
694         }
695
696         return $location;
697     }
698
699     function hasRole($name)
700     {
701         $has_role = false;
702         if (Event::handle('StartHasRole', array($this, $name, &$has_role))) {
703             $role = Profile_role::pkeyGet(array('profile_id' => $this->id,
704                                                 'role' => $name));
705             $has_role = !empty($role);
706             Event::handle('EndHasRole', array($this, $name, $has_role));
707         }
708         return $has_role;
709     }
710
711     function grantRole($name)
712     {
713         $role = new Profile_role();
714
715         $role->profile_id = $this->id;
716         $role->role       = $name;
717         $role->created    = common_sql_now();
718
719         $result = $role->insert();
720
721         if (!$result) {
722             common_log_db_error($role, 'INSERT', __FILE__);
723             return false;
724         }
725
726         return true;
727     }
728
729     function revokeRole($name)
730     {
731         $role = Profile_role::pkeyGet(array('profile_id' => $this->id,
732                                             'role' => $name));
733
734         if (empty($role)) {
735             // TRANS: Exception thrown when trying to revoke an existing role for a user that does not exist.
736             // TRANS: %1$s is the role name, %2$s is the user ID (number).
737             throw new Exception(sprintf(_('Cannot revoke role "%1$s" for user #%2$d; does not exist.'),$name, $this->id));
738         }
739
740         $result = $role->delete();
741
742         if (!$result) {
743             common_log_db_error($role, 'DELETE', __FILE__);
744             // TRANS: Exception thrown when trying to revoke a role for a user with a failing database query.
745             // TRANS: %1$s is the role name, %2$s is the user ID (number).
746             throw new Exception(sprintf(_('Cannot revoke role "%1$s" for user #%2$d; database error.'),$name, $this->id));
747         }
748
749         return true;
750     }
751
752     function isSandboxed()
753     {
754         return $this->hasRole(Profile_role::SANDBOXED);
755     }
756
757     function isSilenced()
758     {
759         return $this->hasRole(Profile_role::SILENCED);
760     }
761
762     function sandbox()
763     {
764         $this->grantRole(Profile_role::SANDBOXED);
765     }
766
767     function unsandbox()
768     {
769         $this->revokeRole(Profile_role::SANDBOXED);
770     }
771
772     function silence()
773     {
774         $this->grantRole(Profile_role::SILENCED);
775     }
776
777     function unsilence()
778     {
779         $this->revokeRole(Profile_role::SILENCED);
780     }
781
782     /**
783      * Does this user have the right to do X?
784      *
785      * With our role-based authorization, this is merely a lookup for whether the user
786      * has a particular role. The implementation currently uses a switch statement
787      * to determine if the user has the pre-defined role to exercise the right. Future
788      * implementations may allow per-site roles, and different mappings of roles to rights.
789      *
790      * @param $right string Name of the right, usually a constant in class Right
791      * @return boolean whether the user has the right in question
792      */
793
794     function hasRight($right)
795     {
796         $result = false;
797         if ($this->hasRole(Profile_role::DELETED)) {
798             return false;
799         }
800         if (Event::handle('UserRightsCheck', array($this, $right, &$result))) {
801             switch ($right)
802             {
803             case Right::DELETEOTHERSNOTICE:
804             case Right::MAKEGROUPADMIN:
805             case Right::SANDBOXUSER:
806             case Right::SILENCEUSER:
807             case Right::DELETEUSER:
808                 $result = $this->hasRole(Profile_role::MODERATOR);
809                 break;
810             case Right::CONFIGURESITE:
811                 $result = $this->hasRole(Profile_role::ADMINISTRATOR);
812                 break;
813             case Right::GRANTROLE:
814             case Right::REVOKEROLE:
815                 $result = $this->hasRole(Profile_role::OWNER);
816                 break;
817             case Right::NEWNOTICE:
818             case Right::NEWMESSAGE:
819             case Right::SUBSCRIBE:
820                 $result = !$this->isSilenced();
821                 break;
822             case Right::PUBLICNOTICE:
823             case Right::EMAILONREPLY:
824             case Right::EMAILONSUBSCRIBE:
825             case Right::EMAILONFAVE:
826                 $result = !$this->isSandboxed();
827                 break;
828             default:
829                 $result = false;
830                 break;
831             }
832         }
833         return $result;
834     }
835
836     function hasRepeated($notice_id)
837     {
838         // XXX: not really a pkey, but should work
839
840         $notice = Memcached_DataObject::pkeyGet('Notice',
841                                                 array('profile_id' => $this->id,
842                                                       'repeat_of' => $notice_id));
843
844         return !empty($notice);
845     }
846
847     /**
848      * Returns an XML string fragment with limited profile information
849      * as an Atom <author> element.
850      *
851      * Assumes that Atom has been previously set up as the base namespace.
852      *
853      * @param Profile $cur the current authenticated user
854      *
855      * @return string
856      */
857     function asAtomAuthor($cur = null)
858     {
859         $xs = new XMLStringer(true);
860
861         $xs->elementStart('author');
862         $xs->element('name', null, $this->nickname);
863         $xs->element('uri', null, $this->getUri());
864         if ($cur != null) {
865             $attrs = Array();
866             $attrs['following'] = $cur->isSubscribed($this) ? 'true' : 'false';
867             $attrs['blocking']  = $cur->hasBlocked($this) ? 'true' : 'false';
868             $xs->element('statusnet:profile_info', $attrs, null);
869         }
870         $xs->elementEnd('author');
871
872         return $xs->getString();
873     }
874
875     /**
876      * Returns an XML string fragment with profile information as an
877      * Activity Streams <activity:actor> element.
878      *
879      * Assumes that 'activity' namespace has been previously defined.
880      *
881      * @return string
882      */
883     function asActivityActor()
884     {
885         return $this->asActivityNoun('actor');
886     }
887
888     /**
889      * Returns an XML string fragment with profile information as an
890      * Activity Streams noun object with the given element type.
891      *
892      * Assumes that 'activity', 'georss', and 'poco' namespace has been
893      * previously defined.
894      *
895      * @param string $element one of 'actor', 'subject', 'object', 'target'
896      *
897      * @return string
898      */
899     function asActivityNoun($element)
900     {
901         $noun = ActivityObject::fromProfile($this);
902         return $noun->asString('activity:' . $element);
903     }
904
905     /**
906      * Returns the best URI for a profile. Plugins may override.
907      *
908      * @return string $uri
909      */
910     function getUri()
911     {
912         $uri = null;
913
914         // give plugins a chance to set the URI
915         if (Event::handle('StartGetProfileUri', array($this, &$uri))) {
916
917             // check for a local user first
918             $user = User::staticGet('id', $this->id);
919
920             if (!empty($user)) {
921                 $uri = $user->uri;
922             } else {
923                 // return OMB profile if any
924                 $remote = Remote_profile::staticGet('id', $this->id);
925                 if (!empty($remote)) {
926                     $uri = $remote->uri;
927                 }
928             }
929             Event::handle('EndGetProfileUri', array($this, &$uri));
930         }
931
932         return $uri;
933     }
934
935     function hasBlocked($other)
936     {
937         $block = Profile_block::get($this->id, $other->id);
938
939         if (empty($block)) {
940             $result = false;
941         } else {
942             $result = true;
943         }
944
945         return $result;
946     }
947
948     function getAtomFeed()
949     {
950         $feed = null;
951
952         if (Event::handle('StartProfileGetAtomFeed', array($this, &$feed))) {
953             $user = User::staticGet('id', $this->id);
954             if (!empty($user)) {
955                 $feed = common_local_url('ApiTimelineUser', array('id' => $user->id,
956                                                                   'format' => 'atom'));
957             }
958             Event::handle('EndProfileGetAtomFeed', array($this, $feed));
959         }
960
961         return $feed;
962     }
963
964     static function fromURI($uri)
965     {
966         $profile = null;
967
968         if (Event::handle('StartGetProfileFromURI', array($uri, &$profile))) {
969             // Get a local user or remote (OMB 0.1) profile
970             $user = User::staticGet('uri', $uri);
971             if (!empty($user)) {
972                 $profile = $user->getProfile();
973             } else {
974                 $remote_profile = Remote_profile::staticGet('uri', $uri);
975                 if (!empty($remote_profile)) {
976                     $profile = Profile::staticGet('id', $remote_profile->profile_id);
977                 }
978             }
979             Event::handle('EndGetProfileFromURI', array($uri, $profile));
980         }
981
982         return $profile;
983     }
984 }