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