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