Nickname::exists adaptation and registration stuff
[statusnet:freesocial.git] / classes / User.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')) {
21     exit(1);
22 }
23
24 /**
25  * Table Definition for user
26  */
27
28 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
29 require_once 'Validate.php';
30
31 class User extends Managed_DataObject
32 {
33     const SUBSCRIBE_POLICY_OPEN = 0;
34     const SUBSCRIBE_POLICY_MODERATE = 1;
35
36     ###START_AUTOCODE
37     /* the code below is auto generated do not remove the above tag */
38
39     public $__table = 'user';                            // table name
40     public $id;                              // int(4)  primary_key not_null
41     public $nickname;                        // varchar(64)  unique_key
42     public $password;                        // varchar(255)
43     public $email;                           // varchar(255)  unique_key
44     public $incomingemail;                   // varchar(255)  unique_key
45     public $emailnotifysub;                  // tinyint(1)   default_1
46     public $emailnotifyfav;                  // tinyint(1)   default_1
47     public $emailnotifymsg;                  // tinyint(1)   default_1
48     public $emailnotifyattn;                 // tinyint(1)   default_1
49     public $emailmicroid;                    // tinyint(1)   default_1
50     public $language;                        // varchar(50)
51     public $timezone;                        // varchar(50)
52     public $emailpost;                       // tinyint(1)   default_1
53     public $uri;                             // varchar(255)  unique_key
54     public $autosubscribe;                   // tinyint(1)
55     public $subscribe_policy;                // tinyint(1)
56     public $urlshorteningservice;            // varchar(50)   default_ur1.ca
57     public $inboxed;                         // tinyint(1)
58     public $private_stream;                  // tinyint(1)   default_0
59     public $created;                         // datetime()   not_null
60     public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
61
62     /* Static get */
63     function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('User',$k,$v); }
64
65     /* the code above is auto generated do not remove the tag below */
66     ###END_AUTOCODE
67
68     public static function schemaDef()
69     {
70         return array(
71             'description' => 'local users',
72             'fields' => array(
73                 'id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'),
74                 'nickname' => array('type' => 'varchar', 'length' => 64, 'description' => 'nickname or username, duped in profile'),
75                 'password' => array('type' => 'varchar', 'length' => 255, 'description' => 'salted password, can be null for OpenID users'),
76                 'email' => array('type' => 'varchar', 'length' => 255, 'description' => 'email address for password recovery etc.'),
77                 'incomingemail' => array('type' => 'varchar', 'length' => 255, 'description' => 'email address for post-by-email'),
78                 'emailnotifysub' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of subscriptions'),
79                 'emailnotifyfav' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of favorites'),
80                 'emailnotifymsg' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of direct messages'),
81                 'emailnotifyattn' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of @-mentions'),
82                 'emailmicroid' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'whether to publish email microid'),
83                 'language' => array('type' => 'varchar', 'length' => 50, 'description' => 'preferred language'),
84                 'timezone' => array('type' => 'varchar', 'length' => 50, 'description' => 'timezone'),
85                 'emailpost' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Post by email'),
86                 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'),
87                 'autosubscribe' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'automatically subscribe to users who subscribe to us'),
88                 'subscribe_policy' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => '0 = anybody can subscribe; 1 = require approval'),
89                 'urlshorteningservice' => array('type' => 'varchar', 'length' => 50, 'default' => 'internal', 'description' => 'service to use for auto-shortening URLs'),
90                 'inboxed' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'has an inbox been created for this user?'),
91                 'private_stream' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'whether to limit all notices to followers only'),
92
93                 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
94                 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
95             ),
96             'primary key' => array('id'),
97             'unique keys' => array(
98                 'user_nickname_key' => array('nickname'),
99                 'user_email_key' => array('email'),
100                 'user_incomingemail_key' => array('incomingemail'),
101                 'user_uri_key' => array('uri'),
102             ),
103             'foreign keys' => array(
104                 'user_id_fkey' => array('profile', array('id' => 'id')),
105             ),
106         );
107     }
108
109     protected $_profile = -1;
110
111     /**
112      * @return Profile
113      */
114     function getProfile()
115     {
116         if (is_int($this->_profile) && $this->_profile == -1) { // invalid but distinct from null
117             $this->_profile = Profile::staticGet('id', $this->id);
118             if (empty($this->_profile)) {
119                 throw new UserNoProfileException($this);
120             }
121         }
122
123         return $this->_profile;
124     }
125
126     function isSubscribed($other)
127     {
128         $profile = $this->getProfile();
129         return $profile->isSubscribed($other);
130     }
131
132     function hasPendingSubscription($other)
133     {
134         $profile = $this->getProfile();
135         return $profile->hasPendingSubscription($other);
136     }
137
138     // 'update' won't write key columns, so we have to do it ourselves.
139
140     function updateKeys(&$orig)
141     {
142         $this->_connect();
143         $parts = array();
144         foreach (array('nickname', 'email', 'incomingemail', 'language', 'timezone') as $k) {
145             if (strcmp($this->$k, $orig->$k) != 0) {
146                 $parts[] = $k . ' = ' . $this->_quote($this->$k);
147             }
148         }
149         if (count($parts) == 0) {
150             // No changes
151             return true;
152         }
153         $toupdate = implode(', ', $parts);
154
155         $table = common_database_tablename($this->tableName());
156         $qry = 'UPDATE ' . $table . ' SET ' . $toupdate .
157           ' WHERE id = ' . $this->id;
158         $orig->decache();
159         $result = $this->query($qry);
160         if ($result) {
161             $this->encache();
162         }
163         return $result;
164     }
165
166     /**
167      * Check whether the given nickname is potentially usable, or if it's
168      * excluded by any blacklists on this system.
169      *
170      * WARNING: INPUT IS NOT VALIDATED OR NORMALIZED. NON-NORMALIZED INPUT
171      * OR INVALID INPUT MAY LEAD TO FALSE RESULTS.
172      *
173      * @param string $nickname
174      * @return boolean true if clear, false if blacklisted
175      */
176     static function allowed_nickname($nickname)
177     {
178         // XXX: should already be validated for size, content, etc.
179         $blacklist = common_config('nickname', 'blacklist');
180
181         //all directory and file names should be blacklisted
182         $d = dir(INSTALLDIR);
183         while (false !== ($entry = $d->read())) {
184             $blacklist[]=$entry;
185         }
186         $d->close();
187
188         //all top level names in the router should be blacklisted
189         $router = Router::get();
190         foreach(array_keys($router->m->getPaths()) as $path){
191             if(preg_match('/^\/(.*?)[\/\?]/',$path,$matches)){
192                 $blacklist[]=$matches[1];
193             }
194         }
195         return !in_array($nickname, $blacklist);
196     }
197
198     /**
199      * Get the most recent notice posted by this user, if any.
200      *
201      * @return mixed Notice or null
202      */
203     function getCurrentNotice()
204     {
205         $profile = $this->getProfile();
206         return $profile->getCurrentNotice();
207     }
208
209     /**
210      * @deprecated use Subscription::start($sub, $other);
211      */
212     function subscribeTo($other)
213     {
214         return Subscription::start($this->getProfile(), $other);
215     }
216
217     function hasBlocked($other)
218     {
219         $profile = $this->getProfile();
220         return $profile->hasBlocked($other);
221     }
222
223     /**
224      * Register a new user account and profile and set up default subscriptions.
225      * If a new-user welcome message is configured, this will be sent.
226      *
227      * @param array $fields associative array of optional properties
228      *              string 'bio'
229      *              string 'email'
230      *              bool 'email_confirmed' pass true to mark email as pre-confirmed
231      *              string 'fullname'
232      *              string 'homepage'
233      *              string 'location' informal string description of geolocation
234      *              float 'lat' decimal latitude for geolocation
235      *              float 'lon' decimal longitude for geolocation
236      *              int 'location_id' geoname identifier
237      *              int 'location_ns' geoname namespace to interpret location_id
238      *              string 'nickname' REQUIRED
239      *              string 'password' (may be missing for eg OpenID registrations)
240      *              string 'code' invite code
241      *              ?string 'uri' permalink to notice; defaults to local notice URL
242      * @return mixed User object or false on failure
243      */
244     static function register($fields) {
245
246         // MAGICALLY put fields into current scope
247
248         extract($fields);
249
250         if(!empty($email))
251         {
252             $email = common_canonical_email($email);
253         }
254
255         $nickname = common_canonical_nickname($nickname);
256         if (!User::allowed_nickname($nickname)){
257             common_log(LOG_WARNING, sprintf("Attempted to register a nickname that is not allowed: %s", $nickname),
258                        __FILE__);
259             return false;
260         }
261                 if (Nickname::exists($nickname)) {
262                         throw new Exception(_m('Nickname already in use'));
263                 }
264
265         $profile = new Profile();
266         $profile->nickname = $nickname;
267         $profile->type = Profile::USER;
268         $profile->profileurl = common_profile_url($nickname);
269
270         if (!empty($fullname)) {
271             $profile->fullname = $fullname;
272         }
273         if (!empty($homepage)) {
274             $profile->homepage = $homepage;
275         }
276         if (!empty($bio)) {
277             $profile->bio = $bio;
278         }
279         if (!empty($location)) {
280             $profile->location = $location;
281
282             $loc = Location::fromName($location);
283
284             if (!empty($loc)) {
285                 $profile->lat         = $loc->lat;
286                 $profile->lon         = $loc->lon;
287                 $profile->location_id = $loc->location_id;
288                 $profile->location_ns = $loc->location_ns;
289             }
290         }
291
292         $profile->created = common_sql_now();
293
294         $user = new User();
295
296         $user->nickname = $nickname;
297
298         $invite = null;
299
300         // Users who respond to invite email have proven their ownership of that address
301
302         if (!empty($code)) {
303             $invite = Invitation::staticGet($code);
304             if ($invite && $invite->address && $invite->address_type == 'email' && $invite->address == $email) {
305                 $user->email = $invite->address;
306             }
307         }
308
309         if(isset($email_confirmed) && $email_confirmed) {
310             $user->email = $email;
311         }
312
313         // This flag is ignored but still set to 1
314
315         $user->inboxed = 1;
316
317         // Set default-on options here, otherwise they'll be disabled
318         // initially for sites using caching, since the initial encache
319         // doesn't know about the defaults in the database.
320         $user->emailnotifysub = 1;
321         $user->emailnotifyfav = 1;
322         $user->emailnotifymsg = 1;
323         $user->emailnotifyattn = 1;
324         $user->emailmicroid = 1;
325         $user->emailpost = 1;
326         $user->jabbermicroid = 1;
327
328         $user->created = common_sql_now();
329
330         if (Event::handle('StartUserRegister', array(&$user, &$profile))) {
331
332             $profile->query('BEGIN');
333
334             $id = $profile->insert();
335
336             if (empty($id)) {
337                 common_log_db_error($profile, 'INSERT', __FILE__);
338                 return false;
339             }
340
341             $user->id = $id;
342
343             if (!empty($uri)) {
344                 $user->uri = $uri;
345             } else {
346                 $user->uri = common_user_uri($user);
347             }
348
349             if (!empty($password)) { // may not have a password for OpenID users
350                 $user->password = common_munge_password($password, $id);
351             }
352
353             $result = $user->insert();
354
355             if (!$result) {
356                 common_log_db_error($user, 'INSERT', __FILE__);
357                 return false;
358             }
359
360             // Everyone gets an inbox
361
362             $inbox = new Inbox();
363
364             $inbox->user_id = $user->id;
365             $inbox->notice_ids = '';
366
367             $result = $inbox->insert();
368
369             if (!$result) {
370                 common_log_db_error($inbox, 'INSERT', __FILE__);
371                 return false;
372             }
373
374             // Everyone is subscribed to themself
375
376             $subscription = new Subscription();
377             $subscription->subscriber = $user->id;
378             $subscription->subscribed = $user->id;
379             $subscription->created = $user->created;
380
381             $result = $subscription->insert();
382
383             if (!$result) {
384                 common_log_db_error($subscription, 'INSERT', __FILE__);
385                 return false;
386             }
387
388             // Mark that this invite was converted
389
390             if (!empty($invite)) {
391                 $invite->convert($user);
392             }
393
394             if (!empty($email) && !$user->email) {
395
396                 $confirm = new Confirm_address();
397                 $confirm->code = common_confirmation_code(128);
398                 $confirm->user_id = $user->id;
399                 $confirm->address = $email;
400                 $confirm->address_type = 'email';
401
402                 $result = $confirm->insert();
403
404                 if (!$result) {
405                     common_log_db_error($confirm, 'INSERT', __FILE__);
406                     return false;
407                 }
408             }
409
410             if (!empty($code) && $user->email) {
411                 $user->emailChanged();
412             }
413
414             // Default system subscription
415
416             $defnick = common_config('newuser', 'default');
417
418             if (!empty($defnick)) {
419                 $defuser = User::staticGet('nickname', $defnick);
420                 if (empty($defuser)) {
421                     common_log(LOG_WARNING, sprintf("Default user %s does not exist.", $defnick),
422                                __FILE__);
423                 } else {
424                     Subscription::start($user, $defuser);
425                 }
426             }
427
428             $profile->query('COMMIT');
429
430             if (!empty($email) && !$user->email) {
431                 mail_confirm_address($user, $confirm->code, $profile->nickname, $email);
432             }
433
434             // Welcome message
435
436             $welcome = common_config('newuser', 'welcome');
437
438             if (!empty($welcome)) {
439                 $welcomeuser = User::staticGet('nickname', $welcome);
440                 if (empty($welcomeuser)) {
441                     common_log(LOG_WARNING, sprintf("Welcome user %s does not exist.", $defnick),
442                                __FILE__);
443                 } else {
444                     $notice = Notice::saveNew($welcomeuser->id,
445                                               // TRANS: Notice given on user registration.
446                                               // TRANS: %1$s is the sitename, $2$s is the registering user's nickname.
447                                               sprintf(_('Welcome to %1$s, @%2$s!'),
448                                                       common_config('site', 'name'),
449                                                       $user->nickname),
450                                               'system');
451                 }
452             }
453
454             Event::handle('EndUserRegister', array(&$profile, &$user));
455         }
456
457         return $user;
458     }
459
460     // Things we do when the email changes
461     function emailChanged()
462     {
463
464         $invites = new Invitation();
465         $invites->address = $this->email;
466         $invites->address_type = 'email';
467
468         if ($invites->find()) {
469             while ($invites->fetch()) {
470                 $other = User::staticGet($invites->user_id);
471                 subs_subscribe_to($other, $this);
472             }
473         }
474     }
475
476     function hasFave($notice)
477     {
478         $profile = $this->getProfile();
479         return $profile->hasFave($notice);
480     }
481
482     function mutuallySubscribed($other)
483     {
484         $profile = $this->getProfile();
485         return $profile->mutuallySubscribed($other);
486     }
487
488     function mutuallySubscribedUsers()
489     {
490         // 3-way join; probably should get cached
491         $UT = common_config('db','type')=='pgsql'?'"user"':'user';
492         $qry = "SELECT $UT.* " .
493           "FROM subscription sub1 JOIN $UT ON sub1.subscribed = $UT.id " .
494           "JOIN subscription sub2 ON $UT.id = sub2.subscriber " .
495           'WHERE sub1.subscriber = %d and sub2.subscribed = %d ' .
496           "ORDER BY $UT.nickname";
497         $user = new User();
498         $user->query(sprintf($qry, $this->id, $this->id));
499
500         return $user;
501     }
502
503     function getMentions($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
504     {
505         return Mention::stream($this->id, $offset, $limit, $since_id, $before_id);
506     }
507
508     function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) {
509         $profile = $this->getProfile();
510         return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id);
511     }
512
513     function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
514     {
515         $profile = $this->getProfile();
516         return $profile->getNotices($offset, $limit, $since_id, $before_id);
517     }
518
519     function favoriteNotices($own=false, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
520     {
521         return Fave::stream($this->id, $offset, $limit, $own, $since_id, $max_id);
522     }
523
524     function noticeInbox($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
525     {
526         $stream = new InboxNoticeStream($this);
527         return $stream->getNotices($offset, $limit, $since_id, $before_id);
528     }
529
530     // DEPRECATED, use noticeInbox()
531
532     function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
533     {
534         return $this->noticeInbox($offset, $limit, $since_id, $before_id);
535     }
536
537     // DEPRECATED, use noticeInbox()
538
539     function noticesWithFriendsThreaded($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
540     {
541         return $this->noticeInbox($offset, $limit, $since_id, $before_id);
542     }
543
544     // DEPRECATED, use noticeInbox()
545
546     function noticeInboxThreaded($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
547     {
548         return $this->noticeInbox($offset, $limit, $since_id, $before_id);
549     }
550
551     // DEPRECATED, use noticeInbox()
552
553     function friendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
554     {
555         return $this->noticeInbox($offset, $limit, $since_id, $before_id);
556     }
557
558     // DEPRECATED, use noticeInbox()
559
560     function ownFriendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
561     {
562         $this->noticeInbox($offset, $limit, $since_id, $before_id);
563     }
564
565     function blowFavesCache()
566     {
567         $profile = $this->getProfile();
568         $profile->blowFavesCache();
569     }
570
571     function getSelfTags()
572     {
573         return Profile_tag::getTagsArray($this->id, $this->id, $this->id);
574     }
575
576     function setSelfTags($newtags, $privacy)
577     {
578         return Profile_tag::setTags($this->id, $this->id, $newtags, $privacy);
579     }
580
581     function block($other)
582     {
583         // Add a new block record
584
585         // no blocking (and thus unsubbing from) yourself
586
587         if ($this->id == $other->id) {
588             common_log(LOG_WARNING,
589                 sprintf(
590                     "Profile ID %d (%s) tried to block themself.",
591                     $this->id,
592                     $this->nickname
593                 )
594             );
595             return false;
596         }
597
598         $block = new Profile_block();
599
600         // Begin a transaction
601
602         $block->query('BEGIN');
603
604         $block->blocker = $this->id;
605         $block->blocked = $other->id;
606
607         $result = $block->insert();
608
609         if (!$result) {
610             common_log_db_error($block, 'INSERT', __FILE__);
611             return false;
612         }
613
614         $self = $this->getProfile();
615         if (Subscription::exists($other, $self)) {
616             Subscription::cancel($other, $self);
617         }
618         if (Subscription::exists($self, $other)) {
619             Subscription::cancel($self, $other);
620         }
621
622         $block->query('COMMIT');
623
624         return true;
625     }
626
627     function unblock($other)
628     {
629         // Get the block record
630
631         $block = Profile_block::get($this->id, $other->id);
632
633         if (!$block) {
634             return false;
635         }
636
637         $result = $block->delete();
638
639         if (!$result) {
640             common_log_db_error($block, 'DELETE', __FILE__);
641             return false;
642         }
643
644         return true;
645     }
646
647     function isMember($group)
648     {
649         $profile = $this->getProfile();
650         return $profile->isMember($group);
651     }
652
653     function isAdmin($group)
654     {
655         $profile = $this->getProfile();
656         return $profile->isAdmin($group);
657     }
658
659     function getGroups($offset=0, $limit=null)
660     {
661         $profile = $this->getProfile();
662         return $profile->getGroups($offset, $limit);
663     }
664
665     /**
666      * Request to join the given group.
667      * May throw exceptions on failure.
668      *
669      * @param User_group $group
670      * @return Group_member
671      */
672     function joinGroup(User_group $group)
673     {
674         $profile = $this->getProfile();
675         return $profile->joinGroup($group);
676     }
677
678     /**
679      * Leave a group that this user is a member of.
680      *
681      * @param User_group $group
682      */
683     function leaveGroup(User_group $group)
684     {
685         $profile = $this->getProfile();
686         return $profile->leaveGroup($group);
687     }
688
689     function getSubscriptions($offset=0, $limit=null)
690     {
691         $profile = $this->getProfile();
692         return $profile->getSubscriptions($offset, $limit);
693     }
694
695     function getSubscribers($offset=0, $limit=null)
696     {
697         $profile = $this->getProfile();
698         return $profile->getSubscribers($offset, $limit);
699     }
700
701     function getTaggedSubscribers($tag, $offset=0, $limit=null)
702     {
703         $qry =
704           'SELECT profile.* ' .
705           'FROM profile JOIN subscription ' .
706           'ON profile.id = subscription.subscriber ' .
707           'JOIN profile_tag ON (profile_tag.tagged = subscription.subscriber ' .
708           'AND profile_tag.tagger = subscription.subscribed) ' .
709           'WHERE subscription.subscribed = %d ' .
710           "AND profile_tag.tag = '%s' " .
711           'AND subscription.subscribed != subscription.subscriber ' .
712           'ORDER BY subscription.created DESC ';
713
714         if ($offset) {
715             $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
716         }
717
718         $profile = new Profile();
719
720         $cnt = $profile->query(sprintf($qry, $this->id, $tag));
721
722         return $profile;
723     }
724
725     function getTaggedSubscriptions($tag, $offset=0, $limit=null)
726     {
727         $qry =
728           'SELECT profile.* ' .
729           'FROM profile JOIN subscription ' .
730           'ON profile.id = subscription.subscribed ' .
731           'JOIN profile_tag on (profile_tag.tagged = subscription.subscribed ' .
732           'AND profile_tag.tagger = subscription.subscriber) ' .
733           'WHERE subscription.subscriber = %d ' .
734           "AND profile_tag.tag = '%s' " .
735           'AND subscription.subscribed != subscription.subscriber ' .
736           'ORDER BY subscription.created DESC ';
737
738         $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
739
740         $profile = new Profile();
741
742         $profile->query(sprintf($qry, $this->id, $tag));
743
744         return $profile;
745     }
746
747     function hasRight($right)
748     {
749         $profile = $this->getProfile();
750         return $profile->hasRight($right);
751     }
752
753     function delete()
754     {
755         try {
756             $profile = $this->getProfile();
757             $profile->delete();
758         } catch (UserNoProfileException $unp) {
759             common_log(LOG_INFO, "User {$this->nickname} has no profile; continuing deletion.");
760         }
761
762         $related = array('Fave',
763                          'Confirm_address',
764                          'Remember_me',
765                          'Foreign_link',
766                          'Invitation',
767                          );
768
769         Event::handle('UserDeleteRelated', array($this, &$related));
770
771         foreach ($related as $cls) {
772             $inst = new $cls();
773             $inst->user_id = $this->id;
774             $inst->delete();
775         }
776
777         $this->_deleteTags();
778         $this->_deleteBlocks();
779
780         parent::delete();
781     }
782
783     function _deleteTags()
784     {
785         $tag = new Profile_tag();
786         $tag->tagger = $this->id;
787         $tag->delete();
788     }
789
790     function _deleteBlocks()
791     {
792         $block = new Profile_block();
793         $block->blocker = $this->id;
794         $block->delete();
795         // XXX delete group block? Reset blocker?
796     }
797
798     function hasRole($name)
799     {
800         $profile = $this->getProfile();
801         return $profile->hasRole($name);
802     }
803
804     function grantRole($name)
805     {
806         $profile = $this->getProfile();
807         return $profile->grantRole($name);
808     }
809
810     function revokeRole($name)
811     {
812         $profile = $this->getProfile();
813         return $profile->revokeRole($name);
814     }
815
816     function isSandboxed()
817     {
818         $profile = $this->getProfile();
819         return $profile->isSandboxed();
820     }
821
822     function isSilenced()
823     {
824         $profile = $this->getProfile();
825         return $profile->isSilenced();
826     }
827
828     function repeatedByMe($offset=0, $limit=20, $since_id=null, $max_id=null)
829     {
830         $stream = new RepeatedByMeNoticeStream($this->id);
831         return $stream->getNotices($offset, $limit, $since_id, $max_id);
832     }
833
834
835     function repeatsOfMe($offset=0, $limit=20, $since_id=null, $max_id=null)
836     {
837         $stream = new RepeatsOfMeNoticeStream($this->id);
838
839         return $stream->getNotices($offset, $limit, $since_id, $max_id);
840     }
841
842
843     function repeatedToMe($offset=0, $limit=20, $since_id=null, $max_id=null)
844     {
845         // TRANS: Exception thrown when trying view "repeated to me".
846         throw new Exception(_('Not implemented since inbox change.'));
847     }
848
849     function shareLocation()
850     {
851         $cfg = common_config('location', 'share');
852
853         if ($cfg == 'always') {
854             return true;
855         } else if ($cfg == 'never') {
856             return false;
857         } else { // user
858             $share = true;
859
860             $prefs = User_location_prefs::staticGet('user_id', $this->id);
861
862             if (empty($prefs)) {
863                 $share = common_config('location', 'sharedefault');
864             } else {
865                 $share = $prefs->share_location;
866                 $prefs->free();
867             }
868
869             return $share;
870         }
871     }
872
873     static function siteOwner()
874     {
875         $owner = self::cacheGet('user:site_owner');
876
877         if ($owner === false) { // cache miss
878
879             $pr = new Profile_role();
880
881             $pr->role = Profile_role::OWNER;
882
883             $pr->orderBy('created');
884
885             $pr->limit(1);
886
887             if ($pr->find(true)) {
888                 $owner = User::staticGet('id', $pr->profile_id);
889             } else {
890                 $owner = null;
891             }
892
893             self::cacheSet('user:site_owner', $owner);
894         }
895
896         return $owner;
897     }
898
899     /**
900      * Pull the primary site account to use in single-user mode.
901      * If a valid user nickname is listed in 'singleuser':'nickname'
902      * in the config, this will be used; otherwise the site owner
903      * account is taken by default.
904      *
905      * @return User
906      * @throws ServerException if no valid single user account is present
907      * @throws ServerException if called when not in single-user mode
908      */
909     static function singleUser()
910     {
911         if (common_config('singleuser', 'enabled')) {
912
913             $user = null;
914
915             $nickname = common_config('singleuser', 'nickname');
916
917             if (!empty($nickname)) {
918                 $user = User::staticGet('nickname', $nickname);
919             }
920
921             // if there was no nickname or no user by that nickname,
922             // try the site owner.
923
924             if (empty($user)) {
925                 $user = User::siteOwner();
926             }
927
928             if (!empty($user)) {
929                 return $user;
930             } else {
931                 // TRANS: Server exception.
932                 throw new ServerException(_('No single user defined for single-user mode.'));
933             }
934         } else {
935             // TRANS: Server exception.
936             throw new ServerException(_('Single-user mode code called when not enabled.'));
937         }
938     }
939
940     /**
941      * This is kind of a hack for using external setup code that's trying to
942      * build single-user sites.
943      *
944      * Will still return a username if the config singleuser/nickname is set
945      * even if the account doesn't exist, which normally indicates that the
946      * site is horribly misconfigured.
947      *
948      * At the moment, we need to let it through so that router setup can
949      * complete, otherwise we won't be able to create the account.
950      *
951      * This will be easier when we can more easily create the account and
952      * *then* switch the site to 1user mode without jumping through hoops.
953      *
954      * @return string
955      * @throws ServerException if no valid single user account is present
956      * @throws ServerException if called when not in single-user mode
957      */
958     static function singleUserNickname()
959     {
960         try {
961             $user = User::singleUser();
962             return $user->nickname;
963         } catch (Exception $e) {
964             if (common_config('singleuser', 'enabled') && common_config('singleuser', 'nickname')) {
965                 common_log(LOG_WARNING, "Warning: code attempting to pull single-user nickname when the account does not exist. If this is not setup time, this is probably a bug.");
966                 return common_config('singleuser', 'nickname');
967             }
968             throw $e;
969         }
970     }
971
972     /**
973      * Find and shorten links in the given text using this user's URL shortening
974      * settings.
975      *
976      * By default, links will be left untouched if the text is shorter than the
977      * configured maximum notice length. Pass true for the $always parameter
978      * to force all links to be shortened regardless.
979      *
980      * Side effects: may save file and file_redirection records for referenced URLs.
981      *
982      * @param string $text
983      * @param boolean $always
984      * @return string
985      */
986     public function shortenLinks($text, $always=false)
987     {
988         return common_shorten_links($text, $always, $this);
989     }
990
991     /*
992      * Get a list of OAuth client applications that have access to this
993      * user's account.
994      */
995     function getConnectedApps($offset = 0, $limit = null)
996     {
997         $qry =
998           'SELECT u.* ' .
999           'FROM oauth_application_user u, oauth_application a ' .
1000           'WHERE u.profile_id = %d ' .
1001           'AND a.id = u.application_id ' .
1002           'AND u.access_type > 0 ' .
1003           'ORDER BY u.created DESC ';
1004
1005         if ($offset > 0) {
1006             if (common_config('db','type') == 'pgsql') {
1007                 $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
1008             } else {
1009                 $qry .= ' LIMIT ' . $offset . ', ' . $limit;
1010             }
1011         }
1012
1013         $apps = new Oauth_application_user();
1014
1015         $cnt = $apps->query(sprintf($qry, $this->id));
1016
1017         return $apps;
1018     }
1019
1020     /**
1021      * Magic function called at serialize() time.
1022      *
1023      * We use this to drop a couple process-specific references
1024      * from DB_DataObject which can cause trouble in future
1025      * processes.
1026      *
1027      * @return array of variable names to include in serialization.
1028      */
1029
1030     function __sleep()
1031     {
1032         $vars = parent::__sleep();
1033         $skip = array('_profile');
1034         return array_diff($vars, $skip);
1035     }
1036
1037     static function recoverPassword($nore)
1038     {
1039         $user = User::staticGet('email', common_canonical_email($nore));
1040
1041         if (!$user) {
1042             try {
1043                 $user = User::staticGet('nickname', common_canonical_nickname($nore));
1044             } catch (NicknameException $e) {
1045                 // invalid
1046             }
1047         }
1048
1049         // See if it's an unconfirmed email address
1050
1051         if (!$user) {
1052             // Warning: it may actually be legit to have multiple folks
1053             // who have claimed, but not yet confirmed, the same address.
1054             // We'll only send to the first one that comes up.
1055             $confirm_email = new Confirm_address();
1056             $confirm_email->address = common_canonical_email($nore);
1057             $confirm_email->address_type = 'email';
1058             $confirm_email->find();
1059             if ($confirm_email->fetch()) {
1060                 $user = User::staticGet($confirm_email->user_id);
1061             } else {
1062                 $confirm_email = null;
1063             }
1064         } else {
1065             $confirm_email = null;
1066         }
1067
1068         if (!$user) {
1069             // TRANS: Information on password recovery form if no known username or e-mail address was specified.
1070             throw new ClientException(_('No user with that email address or username.'));
1071             return;
1072         }
1073
1074         // Try to get an unconfirmed email address if they used a user name
1075
1076         if (!$user->email && !$confirm_email) {
1077             $confirm_email = new Confirm_address();
1078             $confirm_email->user_id = $user->id;
1079             $confirm_email->address_type = 'email';
1080             $confirm_email->find();
1081             if (!$confirm_email->fetch()) {
1082                 $confirm_email = null;
1083             }
1084         }
1085
1086         if (!$user->email && !$confirm_email) {
1087             // TRANS: Client error displayed on password recovery form if a user does not have a registered e-mail address.
1088             throw new ClientException(_('No registered email address for that user.'));
1089             return;
1090         }
1091
1092         // Success! We have a valid user and a confirmed or unconfirmed email address
1093
1094         $confirm = new Confirm_address();
1095         $confirm->code = common_confirmation_code(128);
1096         $confirm->address_type = 'recover';
1097         $confirm->user_id = $user->id;
1098         $confirm->address = (!empty($user->email)) ? $user->email : $confirm_email->address;
1099
1100         if (!$confirm->insert()) {
1101             common_log_db_error($confirm, 'INSERT', __FILE__);
1102             // TRANS: Server error displayed if e-mail address confirmation fails in the database on the password recovery form.
1103             throw new ServerException(_('Error saving address confirmation.'));
1104             return;
1105         }
1106
1107          // @todo FIXME: needs i18n.
1108         $body = "Hey, $user->nickname.";
1109         $body .= "\n\n";
1110         $body .= 'Someone just asked for a new password ' .
1111                  'for this account on ' . common_config('site', 'name') . '.';
1112         $body .= "\n\n";
1113         $body .= 'If it was you, and you want to confirm, use the URL below:';
1114         $body .= "\n\n";
1115         $body .= "\t".common_local_url('recoverpassword',
1116                                    array('code' => $confirm->code));
1117         $body .= "\n\n";
1118         $body .= 'If not, just ignore this message.';
1119         $body .= "\n\n";
1120         $body .= 'Thanks for your time, ';
1121         $body .= "\n";
1122         $body .= common_config('site', 'name');
1123         $body .= "\n";
1124
1125         $headers = _mail_prepare_headers('recoverpassword', $user->nickname, $user->nickname);
1126         // TRANS: Subject for password recovery e-mail.
1127         mail_to_user($user, _('Password recovery requested'), $body, $headers, $confirm->address);
1128     }
1129 }