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