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