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