Nickname::exists adaptation and registration stuff
[statusnet:freesocial.git] / classes / User_group.php
1 <?php
2 /**
3  * Table Definition for user_group
4  */
5
6 class User_group extends Managed_DataObject
7 {
8     const JOIN_POLICY_OPEN = 0;
9     const JOIN_POLICY_MODERATE = 1;
10     const CACHE_WINDOW = 201;
11
12     ###START_AUTOCODE
13     /* the code below is auto generated do not remove the above tag */
14
15     public $__table = 'user_group';                      // table name
16     public $id;                              // int(4)  primary_key not_null
17     public $nickname;                        // varchar(64)
18     public $fullname;                        // varchar(255)
19     public $homepage;                        // varchar(255)
20     public $description;                     // text
21     public $location;                        // varchar(255)
22     public $original_logo;                   // varchar(255)
23     public $homepage_logo;                   // varchar(255)
24     public $stream_logo;                     // varchar(255)
25     public $mini_logo;                       // varchar(255)
26     public $created;                         // datetime   not_null default_0000-00-00%2000%3A00%3A00
27     public $modified;                        // timestamp   not_null default_CURRENT_TIMESTAMP
28     public $uri;                             // varchar(255)  unique_key
29     public $mainpage;                        // varchar(255)
30     public $join_policy;                     // tinyint
31     public $force_scope;                     // tinyint
32
33     /* Static get */
34     function staticGet($k,$v=NULL) {
35         return Memcached_DataObject::staticGet('User_group',$k,$v);
36     }
37     
38     function multiGet($keyCol, $keyVals, $skipNulls=true)
39     {
40         return parent::multiGet('User_group', $keyCol, $keyVals, $skipNulls);
41     }
42
43     /* the code above is auto generated do not remove the tag below */
44     ###END_AUTOCODE
45
46     public static function schemaDef()
47     {
48         return array(
49             'fields' => array(
50                 'id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'),
51                 'nickname' => array('type' => 'varchar', 'length' => 64, 'description' => 'nickname for addressing'),
52
53                 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
54                 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
55
56                 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universal identifier'),
57                 'join_policy' => array('type' => 'int', 'size' => 'tiny', 'description' => '0=open; 1=requires admin approval'),      
58                 'force_scope' => array('type' => 'int', 'size' => 'tiny', 'description' => '0=never,1=sometimes,-1=always'),
59                                 //remove the following
60                 'mainpage' => array('type' => 'varchar', 'length' => 255, 'description' => 'page for group info to link to'),
61                 'original_logo' => array('type' => 'varchar', 'length' => 255, 'description' => 'original size logo'),
62                 'homepage_logo' => array('type' => 'varchar', 'length' => 255, 'description' => 'homepage (profile) size logo'),
63                 'stream_logo' => array('type' => 'varchar', 'length' => 255, 'description' => 'stream-sized logo'),
64                 'mini_logo' => array('type' => 'varchar', 'length' => 255, 'description' => 'mini logo'),
65                 'fullname' => array('type' => 'varchar', 'length' => 255, 'description' => 'display name'),
66                 'homepage' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL, cached so we dont regenerate'),
67                 'description' => array('type' => 'text', 'description' => 'group description'),
68                 'location' => array('type' => 'varchar', 'length' => 255, 'description' => 'related physical location, if any'),
69             ),
70             'primary key' => array('id'),
71             'unique keys' => array(
72                 'group_uri_key' => array('uri'),
73             ),
74                         'indexes' => array(
75                 'group_nickname_key' => array('nickname'),
76                         ),
77             'foreign keys' => array(
78                 'group_id_idx' => array('profile', array('id' => 'id')),
79             ),
80         );
81     }
82
83     function defaultLogo($size)
84     {
85         static $sizenames = array(Avatar::PROFILE_SIZE => 'profile',
86                                   Avatar::STREAM_SIZE => 'stream',
87                                   Avatar::MINI_SIZE => 'mini');
88         return Theme::path('default-avatar-'.$sizenames[$size].'.png');
89     }
90
91     function homeUrl()
92     {
93                 try {
94                         $profile = $this->getProfile();
95                 } catch (UserNoProfileException $e) {
96                 }
97                 if (!empty($profile) && $profile->isGroup) {
98                         return $profile->homeUrl();
99                 }
100                 return null;
101     }
102
103     protected $_profile = -1;
104
105     /**
106      * @return Profile
107      */
108     function getProfile()
109     {
110         if (is_int($this->_profile) && $this->_profile == -1) { // invalid but distinct from null
111             $this->_profile = Profile::staticGet('id', $this->id);
112             if (empty($this->_profile)) {
113                 throw new UserNoProfileException($this);
114             }
115         }
116
117         return $this->_profile;
118     }
119
120     function getUri()
121     {
122         $uri = null;
123         if (Event::handle('StartUserGroupGetUri', array($this, &$uri))) {
124             if (!empty($this->uri)) {
125                 $uri = $this->uri;
126             } else {
127                 $uri = common_local_url('groupbyid',
128                                         array('id' => $this->id));
129             }
130         }
131         Event::handle('EndUserGroupGetUri', array($this, &$uri));
132         return $uri;
133     }
134
135     function permalink()
136     {
137         $url = null;
138         if (Event::handle('StartUserGroupPermalink', array($this, &$url))) {
139             $url = common_local_url('groupbyid',
140                                     array('id' => $this->id));
141         }
142         Event::handle('EndUserGroupPermalink', array($this, &$url));
143         return $url;
144     }
145
146     function getNotices($offset, $limit, $since_id=null, $max_id=null)
147     {
148         $stream = new GroupNoticeStream($this);
149
150         return $stream->getNotices($offset, $limit, $since_id, $max_id);
151     }
152
153
154     function allowedNickname($nickname)
155     {
156         static $blacklist = array('new');
157         return !in_array($nickname, $blacklist);
158     }
159
160     function getMemberIDs($offset=0, $limit=null, $desc=false)
161     {
162         $ids = array();
163         $keypart = sprintf('group:member_ids:%d', $this->id);
164         $idstring = self::cacheGet($keypart);
165
166         if ($idstring !== false) {
167                         $ids = explode(',', $idstring);
168                 } else {
169             $gm = new Group_member();
170     
171             $gm->selectAdd();
172             $gm->selectAdd('profile_id');
173     
174             $gm->group_id = $this->id;
175             $gm->orderBy('created ASC');
176
177             if ($gm->find()) {
178                 while ($gm->fetch()) {
179                     $ids[] = $gm->profile_id;
180                 }
181             }
182             
183             self::cacheSet($keypart, implode(',', $ids));
184         }
185
186         if ($desc==true) {
187             $ids = array_reverse($ids);
188         }
189
190                 if (!is_null($offset) && !is_null($limit)) {
191                 $ids = array_slice($ids, $offset, $limit);
192                 }
193
194         return $ids;
195     }
196
197     function getMembers($offset=0, $limit=null, $desc=false) {
198         $ids = $this->getMemberIDs($offset, $limit, $desc);
199         return Profile::multiGet('id', $ids);
200     }
201
202     /**
203      * Get pending members, who have not yet been approved.
204      *
205      * @param int $offset
206      * @param int $limit
207      * @return Profile
208      */
209     function getRequests($offset=0, $limit=null)
210     {
211         $qry =
212           'SELECT profile.* ' .
213           'FROM profile JOIN group_join_queue '.
214           'ON profile.id = group_join_queue.profile_id ' .
215           'WHERE group_join_queue.group_id = %d ' .
216           'ORDER BY group_join_queue.created DESC ';
217
218         if ($limit != null) {
219             if (common_config('db','type') == 'pgsql') {
220                 $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
221             } else {
222                 $qry .= ' LIMIT ' . $offset . ', ' . $limit;
223             }
224         }
225
226         $members = new Profile();
227
228         $members->query(sprintf($qry, $this->id));
229         return $members;
230     }
231
232     function getMemberCount()
233     {
234         $key = sprintf("group:member_count:%d", $this->id);
235
236         $cnt = self::cacheGet($key);
237
238         if (is_integer($cnt)) {
239             return (int) $cnt;
240         }
241
242         $mem = new Group_member();
243         $mem->group_id = $this->id;
244
245         // XXX: why 'distinct'?
246
247         $cnt = (int) $mem->count('distinct profile_id');
248
249         self::cacheSet($key, $cnt);
250
251         return $cnt;
252     }
253
254     function getBlockedCount()
255     {
256         // XXX: WORM cache this
257
258         $block = new Group_block();
259         $block->group_id = $this->id;
260
261         return $block->count();
262     }
263
264     function getQueueCount()
265     {
266         // XXX: WORM cache this
267
268         $queue = new Group_join_queue();
269         $queue->group_id = $this->id;
270
271         return $queue->count();
272     }
273
274     function getAdmins($offset=0, $limit=null)
275     {
276         $qry =
277           'SELECT profile.* ' .
278           'FROM profile JOIN group_member '.
279           'ON profile.id = group_member.profile_id ' .
280           'WHERE group_member.group_id = %d ' .
281           'AND group_member.is_admin = 1 ' .
282           'ORDER BY group_member.modified ASC ';
283
284         if ($limit != null) {
285             if (common_config('db','type') == 'pgsql') {
286                 $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
287             } else {
288                 $qry .= ' LIMIT ' . $offset . ', ' . $limit;
289             }
290         }
291
292         $admins = new Profile();
293
294         $admins->query(sprintf($qry, $this->id));
295         return $admins;
296     }
297
298     function getBlocked($offset=0, $limit=null)
299     {
300         $qry =
301           'SELECT profile.* ' .
302           'FROM profile JOIN group_block '.
303           'ON profile.id = group_block.blocked ' .
304           'WHERE group_block.group_id = %d ' .
305           'ORDER BY group_block.modified DESC ';
306
307         if ($limit != null) {
308             if (common_config('db','type') == 'pgsql') {
309                 $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
310             } else {
311                 $qry .= ' LIMIT ' . $offset . ', ' . $limit;
312             }
313         }
314
315         $blocked = new Profile();
316
317         $blocked->query(sprintf($qry, $this->id));
318         return $blocked;
319     }
320
321     function setOriginal($filename)
322     {
323         $imagefile = new ImageFile($this->id, Avatar::path($filename));
324
325         $orig = clone($this);
326         $this->original_logo = Avatar::url($filename);
327         $this->homepage_logo = Avatar::url($imagefile->resize(Avatar::PROFILE_SIZE));
328         $this->stream_logo = Avatar::url($imagefile->resize(Avatar::STREAM_SIZE));
329         $this->mini_logo = Avatar::url($imagefile->resize(Avatar::MINI_SIZE));
330         common_debug(common_log_objstring($this));
331         return $this->update($orig);
332     }
333
334     function getBestName()
335     {
336         return ($this->fullname) ? $this->fullname : $this->nickname;
337     }
338
339     /**
340      * Gets the full name (if filled) with nickname as a parenthetical, or the nickname alone
341      * if no fullname is provided.
342      *
343      * @return string
344      */
345     function getFancyName()
346     {
347         if ($this->fullname) {
348             // TRANS: Full name of a profile or group followed by nickname in parens
349             return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname);
350         } else {
351             return $this->nickname;
352         }
353     }
354
355     function getAliases()
356     {
357         $aliases = array();
358
359         // XXX: cache this
360
361         $alias = new Group_alias();
362
363         $alias->group_id = $this->id;
364
365         if ($alias->find()) {
366             while ($alias->fetch()) {
367                 $aliases[] = $alias->alias;
368             }
369         }
370
371         $alias->free();
372
373         return $aliases;
374     }
375
376     function setAliases($newaliases) {
377
378         $newaliases = array_unique($newaliases);
379
380         $oldaliases = $this->getAliases();
381
382         // Delete stuff that's old that not in new
383
384         $to_delete = array_diff($oldaliases, $newaliases);
385
386         // Insert stuff that's in new and not in old
387
388         $to_insert = array_diff($newaliases, $oldaliases);
389
390         $alias = new Group_alias();
391
392         $alias->group_id = $this->id;
393
394         foreach ($to_delete as $delalias) {
395             $alias->alias = $delalias;
396             $result = $alias->delete();
397             if (!$result) {
398                 common_log_db_error($alias, 'DELETE', __FILE__);
399                 return false;
400             }
401         }
402
403         foreach ($to_insert as $insalias) {
404             $alias->alias = $insalias;
405             $result = $alias->insert();
406             if (!$result) {
407                 common_log_db_error($alias, 'INSERT', __FILE__);
408                 return false;
409             }
410         }
411
412         return true;
413     }
414
415     static function getForNickname($nickname, $profile=null)
416     {
417         $nickname = common_canonical_nickname($nickname);
418
419         // Are there any matching remote groups this profile's in?
420         if ($profile) {
421             $group = $profile->getGroups(0, null);
422             while ($group->fetch()) {
423                 if ($group->nickname == $nickname) {
424                     // @fixme is this the best way?
425                     return clone($group);
426                 }
427             }
428         }
429
430         // If not, check local groups.
431
432         $group = Local_group::staticGet('nickname', $nickname);
433         if (!empty($group)) {
434             return User_group::staticGet('id', $group->group_id);
435         }
436         $alias = Group_alias::staticGet('alias', $nickname);
437         if (!empty($alias)) {
438             return User_group::staticGet('id', $alias->group_id);
439         }
440         return null;
441     }
442
443     function getUserMembers()
444     {
445         // XXX: cache this
446
447         $user = new User();
448         if(common_config('db','quote_identifiers'))
449             $user_table = '"user"';
450         else $user_table = 'user';
451
452         $qry =
453           'SELECT id ' .
454           'FROM '. $user_table .' JOIN group_member '.
455           'ON '. $user_table .'.id = group_member.profile_id ' .
456           'WHERE group_member.group_id = %d ';
457
458         $user->query(sprintf($qry, $this->id));
459
460         $ids = array();
461
462         while ($user->fetch()) {
463             $ids[] = $user->id;
464         }
465
466         $user->free();
467
468         return $ids;
469     }
470
471     static function maxDescription()
472     {
473         $desclimit = common_config('group', 'desclimit');
474         // null => use global limit (distinct from 0!)
475         if (is_null($desclimit)) {
476             $desclimit = common_config('site', 'textlimit');
477         }
478         return $desclimit;
479     }
480
481     static function descriptionTooLong($desc)
482     {
483         $desclimit = self::maxDescription();
484         return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
485     }
486
487     function asAtomEntry($namespace=false, $source=false)
488     {
489         $xs = new XMLStringer(true);
490
491         if ($namespace) {
492             $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
493                            'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
494         } else {
495             $attrs = array();
496         }
497
498         $xs->elementStart('entry', $attrs);
499
500         if ($source) {
501             $xs->elementStart('source');
502             $xs->element('id', null, $this->permalink());
503             $xs->element('title', null, $profile->nickname . " - " . common_config('site', 'name'));
504             $xs->element('link', array('href' => $this->permalink()));
505             $xs->element('updated', null, $this->modified);
506             $xs->elementEnd('source');
507         }
508
509         $xs->element('title', null, $this->nickname);
510         $xs->element('summary', null, common_xml_safe_str($this->description));
511
512         $xs->element('link', array('rel' => 'alternate',
513                                    'href' => $this->permalink()));
514
515         $xs->element('id', null, $this->permalink());
516
517         $xs->element('published', null, common_date_w3dtf($this->created));
518         $xs->element('updated', null, common_date_w3dtf($this->modified));
519
520         $xs->element(
521             'content',
522             array('type' => 'html'),
523             common_xml_safe_str($this->description)
524         );
525
526         $xs->elementEnd('entry');
527
528         return $xs->getString();
529     }
530
531     function asAtomAuthor()
532     {
533         $xs = new XMLStringer(true);
534
535         $xs->elementStart('author');
536         $xs->element('name', null, $this->nickname);
537         $xs->element('uri', null, $this->permalink());
538         $xs->elementEnd('author');
539
540         return $xs->getString();
541     }
542
543     /**
544      * Returns an XML string fragment with group information as an
545      * Activity Streams <activity:subject> element.
546      *
547      * Assumes that 'activity' namespace has been previously defined.
548      *
549      * @return string
550      */
551     function asActivitySubject()
552     {
553         return $this->asActivityNoun('subject');
554     }
555
556     /**
557      * Returns an XML string fragment with group information as an
558      * Activity Streams noun object with the given element type.
559      *
560      * Assumes that 'activity', 'georss', and 'poco' namespace has been
561      * previously defined.
562      *
563      * @param string $element one of 'actor', 'subject', 'object', 'target'
564      *
565      * @return string
566      */
567     function asActivityNoun($element)
568     {
569         $noun = ActivityObject::fromGroup($this);
570         return $noun->asString('activity:' . $element);
571     }
572
573     function getOriginal()
574     {
575         return empty($this->homepage_logo)
576             ? User_group::defaultLogo(Avatar::PROFILE_SIZE)
577             : $this->homepage_logo;
578     }
579
580     function getAvatar($size=Avatar::PROFILE_SIZE)
581     {
582         return empty($this->homepage_logo)
583             ? User_group::defaultLogo($size)
584             : $this->homepage_logo;
585     }
586
587     // Throws an Exception if something is bad
588     static function register($fields) {
589         if (!empty($fields['userid'])) {
590             $scoped = Profile::staticGet('id', $fields['userid']);
591             if ($scoped && !$scoped->hasRight(Right::CREATEGROUP)) {
592                 common_log(LOG_WARNING, "Attempted group creation from banned user: " . $scoped->nickname);
593
594                 // TRANS: Client exception thrown when a user tries to create a group while banned.
595                 throw new ClientException(_('You are not allowed to create groups on this site.'), 403);
596             }
597         }
598
599         $fields['nickname'] = common_canonical_nickname($fields['nickname']);
600                 if (!User::allowed_nickname($fields['nickname'])) {
601                         common_log(LOG_WARNING, sprintf("Attempted to register a nickname that is not allowed: %s", $profile->nickname), __FILE__);
602                         throw new Exception(_m('Nickname not allowed'));
603                 }
604                 if (Nickname::exists($fields['nickname'])) {
605                         throw new Exception(_m('Nickname already in use'));
606                 }
607
608         $defaults = array('nickname' => null,
609                           'fullname' => null,
610                           'homepage' => null,
611                           'description' => null,
612                           'location' => null,
613                           'uri' => null,
614                           'mainpage' => null);
615
616         // load values into $fields, overwriting as we go
617         $fields = array_merge($defaults, $fields);
618
619         $profile = new Profile();
620
621         $profile->query('BEGIN');
622                 $profile->type = Profile::GROUP;
623
624         if (empty($fields['mainpage'])) {
625             $fields['mainpage'] = common_local_url('showgroup', array('nickname' => $fields['nickname']));
626         }
627
628         // $default contains the Profile keys-to-be-set, $fields has the submitted values
629         foreach(array_keys($defaults) as $key) {
630             $profile->{$key}    = $fields[$key];
631         }
632         $profile->created     = common_sql_now();
633
634                 $result = $profile->insert();
635                 if (!$result) {
636             common_log_db_error($group, 'INSERT', __FILE__);
637             // TRANS: Server exception thrown when creating a group failed.
638             throw new ServerException(_m('Could not create group.'));
639                 }
640
641             
642         if ($fields['local']) {
643             $local_group = new Local_group();
644
645             $local_group->group_id = $profile->id;
646             $local_group->nickname = $profile->nickname;
647             $local_group->created  = $profile->created;
648
649             $result = $local_group->insert();
650
651             if (!$result) {
652                 common_log_db_error($local_group, 'INSERT', __FILE__);
653                 // TRANS: Server exception thrown when saving local group information failed.
654                 throw new ServerException(_('Local group already exists.'));
655             }
656         }
657
658                 $group = new User_group();
659                 $group->id = $profile->id;
660                 $group->nickname = $profile->nickname;
661
662         if (empty($fields['uri'])) {
663             $group->uri = common_local_url('groupbyid', array('id' => $profile->id));
664         }
665
666         $group->join_policy = isset($fields['join_policy'])
667                                                         ? intval($fields['join_policy'])
668                                                         : 0;
669         $group->force_scope = isset($fields['force_scope'])
670                                                         ? intval($fields['force_scope'])
671                                                         : 0;
672
673         if (Event::handle('StartGroupSave', array(&$group))) {
674             $result = $group->insert();
675
676             if (!$result) {
677                 common_log_db_error($group, 'INSERT', __FILE__);
678                 // TRANS: Server exception thrown when creating a group failed.
679                 throw new ServerException(_m('Could not create group.'));
680             }
681
682             $result = $group->setAliases((array)$fields['aliases']);
683
684             if (!$result) {
685                 // TRANS: Server exception thrown when creating group aliases failed.
686                 throw new ServerException(_('Could not create aliases.'));
687             }
688
689             $member = new Group_member();
690
691             $member->group_id   = $profile->id;
692             $member->profile_id = $fields['userid'];
693             $member->is_admin   = 1;
694             $member->created    = $profile->created;
695
696             $result = $member->insert();
697
698             if (!$result) {
699                 common_log_db_error($member, 'INSERT', __FILE__);
700                 // TRANS: Server exception thrown when setting group membership failed.
701                 throw new ServerException(_('Could not set group membership.'));
702             }
703
704             self::blow('profile:groups:%d', $fields['userid']);
705
706             $profile->query('COMMIT');  // finalize everything and write to db
707
708             Event::handle('EndGroupSave', array($group));
709         }
710
711         return $group;
712     }
713
714     /**
715      * Handle cascading deletion, on the model of notice and profile.
716      *
717      * This should handle freeing up cached entries for the group's
718      * id, nickname, URI, and aliases. There may be other areas that
719      * are not de-cached in the UI, including the sidebar lists on
720      * GroupsAction
721      */
722     function delete()
723     {
724         if ($this->id) {
725
726             // Safe to delete in bulk for now
727
728             $related = array('Group_inbox',
729                              'Group_block',
730                              'Group_member',
731                              'Related_group');
732
733             Event::handle('UserGroupDeleteRelated', array($this, &$related));
734
735             foreach ($related as $cls) {
736
737                 $inst = new $cls();
738                 $inst->group_id = $this->id;
739
740                 if ($inst->find()) {
741                     while ($inst->fetch()) {
742                         $dup = clone($inst);
743                         $dup->delete();
744                     }
745                 }
746             }
747
748             // And related groups in the other direction...
749             $inst = new Related_group();
750             $inst->related_group_id = $this->id;
751             $inst->delete();
752
753                         // and the group's profile
754             $inst = new Profile();
755             $inst->id = $this->id;
756             $inst->delete();
757
758             // Aliases and the local_group entry need to be cleared explicitly
759             // or we'll miss clearing some cache keys; that can make it hard
760             // to create a new group with one of those names or aliases.
761             $this->setAliases(array());
762             $local = Local_group::staticGet('group_id', $this->id);
763             if ($local) {
764                 $local->delete();
765             }
766
767             // blow the cached ids
768             self::blow('user_group:notice_ids:%d', $this->id);
769
770         } else {
771             common_log(LOG_WARN, "Ambiguous user_group->delete(); skipping related tables.");
772         }
773         parent::delete();
774     }
775
776     function isPrivate()
777     {
778         return ($this->join_policy == self::JOIN_POLICY_MODERATE &&
779                 $this->force_scope == 1);
780     }
781 }