Tilmeld Server API  1.0.0
Nymph user and group management with access controls.
Group.php
1 <?php namespace Tilmeld\Entities;
2 
4 use Nymph\Nymph;
5 
49 class Group extends AbleObject {
50  const ETYPE = 'tilmeld_group';
51  const DEFAULT_PRIVATE_DATA = [
52  'email',
53  'phone',
54  'addressType',
55  'addressStreet',
56  'addressStreet2',
57  'addressCity',
58  'addressState',
59  'addressZip',
60  'addressInternational',
61  'abilities',
62  'user',
63  ];
64  const DEFAULT_WHITELIST_DATA = [];
65  protected $tags = [];
66  public $clientEnabledMethods = [
67  'checkGroupname',
68  'checkEmail',
69  'getAvatar',
70  'getChildren',
71  'getDescendants',
72  'getLevel',
73  'isDescendant',
74  ];
75  public static $clientEnabledStaticMethods = [
76  'getPrimaryGroups',
77  'getSecondaryGroups',
78  ];
79  protected $privateData = Group::DEFAULT_PRIVATE_DATA;
80  public static $searchRestrictedData = Group::DEFAULT_PRIVATE_DATA;
81  protected $whitelistData = Group::DEFAULT_WHITELIST_DATA;
82  protected $whitelistTags = [];
83 
90  private $skipAcWhenSaving = false;
91 
97  public function __construct($id = 0) {
98  if ((is_int($id) && $id > 0) || is_string($id)) {
99  if (is_int($id)) {
100  $entity = Nymph::getEntity(
101  ['class' => get_class($this)],
102  ['&', 'guid' => $id]
103  );
104  } else {
105  $entity = Nymph::getEntity(
106  ['class' => get_class($this)],
107  ['&',
108  'ilike' => [
109  'groupname',
110  str_replace(['\\', '%', '_'], ['\\\\\\\\', '\%', '\_'], $id)
111  ]
112  ]
113  );
114  }
115  if (isset($entity)) {
116  $this->guid = $entity->guid;
117  $this->tags = $entity->tags;
118  $this->cdate = $entity->cdate;
119  $this->mdate = $entity->mdate;
120  $this->putData($entity->getData(), $entity->getSData());
121  return;
122  }
123  }
124  // Defaults.
125  $this->enabled = true;
126  $this->abilities = [];
127  $this->addressType = 'us';
128  $this->updateDataProtection();
129  }
130 
137  public static function getPrimaryGroups($search = null) {
138  return self::getAssignableGroups(
139  $search,
140  Tilmeld::$config['highest_primary']
141  );
142  }
143 
150  public static function getSecondaryGroups($search = null) {
151  return self::getAssignableGroups(
152  $search,
153  Tilmeld::$config['highest_secondary']
154  );
155  }
156 
157  private static function getAssignableGroups($search, $highestParent) {
158  $assignableGroups = [];
159  if ($search !== null) {
160  $assignableGroups = Nymph::getEntities(
161  ['class' => '\Tilmeld\Entities\Group'],
162  ['&',
163  'equal' => ['enabled', true]
164  ],
165  ['|',
166  'ilike' => [
167  ['name', $search],
168  ['groupname', $search]
169  ],
170  ]
171  );
172  if ($highestParent != 0) {
173  $assignableGroups = array_values(
174  array_filter(
175  $assignableGroups,
176  function ($curGroup) use ($highestParent) {
177  while (isset($curGroup->parent) && $curGroup->parent->guid) {
178  if ($curGroup->parent->guid === $highestParent) {
179  return true;
180  }
181  $curGroup = $curGroup->parent;
182  }
183  return false;
184  }
185  )
186  );
187  }
188  } else {
189  if ($highestParent == 0) {
190  $assignableGroups = Nymph::getEntities(
191  ['class' => '\Tilmeld\Entities\Group'],
192  ['&',
193  'equal' => ['enabled', true]
194  ]
195  );
196  } else {
197  if ($highestParent > 0) {
198  $highestParent = Group::factory($highestParent);
199  if (isset($highestParent->guid)) {
200  $assignableGroups = $highestParent->getDescendants();
201  }
202  }
203  }
204  }
205  return $assignableGroups;
206  }
207 
208  public function getAvatar() {
209  if (isset($this->avatar)) {
210  return $this->avatar;
211  }
212  $proto = $_SERVER['HTTPS'] ? 'https' : 'http';
213  if (!isset($this->email) || empty($this->email)) {
214  return $proto.'://secure.gravatar.com/avatar/?d=mm&s=40';
215  }
216  return $proto.'://secure.gravatar.com/avatar/'.
217  md5(strtolower(trim($this->email))).'?d=identicon&s=40';
218  }
219 
220  public function putData($data, $sdata = []) {
221  $return = parent::putData($data, $sdata);
222  $this->updateDataProtection();
223  return $return;
224  }
225 
233  public function updateDataProtection($user = null) {
234  if (!isset($user)) {
235  $user = User::current();
236  }
237 
238  $this->privateData = self::DEFAULT_PRIVATE_DATA;
239  $this->whitelistData = self::DEFAULT_WHITELIST_DATA;
240 
241  if (Tilmeld::$config['email_usernames']) {
242  $this->privateData[] = 'groupname';
243  }
244  if ($user !== null && $user->gatekeeper('tilmeld/admin')) {
245  // Users who can edit groups can see their data.
246  $this->privateData = [];
247  $this->whitelistData = false;
248  return;
249  }
250  if (isset($this->user) && isset($user) && $this->user->is($user)) {
251  // Users can see their group's data.
252  $this->privateData = [];
253  }
254  }
255 
262  public function isDescendant($group = null) {
263  if (is_numeric($group)) {
264  $group = Group::factory((int) $group);
265  }
266  if (!isset($group->guid)) {
267  return false;
268  }
269  // Check to see if the group is a descendant of the given group.
270  if (!isset($this->parent)) {
271  return false;
272  }
273  if ($this->parent->is($group)) {
274  return true;
275  }
276  if ($this->parent->isDescendant($group)) {
277  return true;
278  }
279  return false;
280  }
281 
287  public function getChildren() {
288  $return = (array) Nymph::getEntities(
289  ['class' => '\Tilmeld\Entities\Group'],
290  ['&',
291  'equal' => ['enabled', true],
292  'ref' => ['parent', $this]
293  ]
294  );
295  return $return;
296  }
297 
304  public function getDescendants($andSelf = false) {
305  $return = [];
306  $entities = Nymph::getEntities(
307  ['class' => '\Tilmeld\Entities\Group'],
308  ['&',
309  'equal' => ['enabled', true],
310  'ref' => ['parent', $this]
311  ]
312  );
313  foreach ($entities as $entity) {
314  $childArray = $entity->getDescendants(true);
315  $return = array_merge($return, $childArray);
316  }
317  $hooked = $this;
318  if (class_exists('\SciActive\Hook')) {
319  $class = get_class();
320  \SciActive\Hook::hookObject($hooked, $class.'->', false);
321  }
322  if ($andSelf) {
323  $return[] = $hooked;
324  }
325  return $return;
326  }
327 
337  public function getLevel() {
338  $group = $this;
339  $level = 0;
340  while (isset($group->parent) && $group->parent->enabled) {
341  $level++;
342  $group = $group->parent;
343  }
344  return $level;
345  }
346 
355  public function getUsers(
356  $descendants = false,
357  $limit = null,
358  $offset = null
359  ) {
360  $groups = [];
361  if ($descendants) {
362  $groups = $this->getDescendants();
363  }
364  $groups[] = $this;
365  $options = ['class' => '\Tilmeld\Entities\User'];
366  if (isset($limit)) {
367  $options['limit'] = $limit;
368  }
369  if (isset($offset)) {
370  $options['offset'] = $offset;
371  }
372  $return = Nymph::getEntities(
373  $options,
374  ['&',
375  'equal' => ['enabled', true]
376  ],
377  ['|',
378  'ref' => [
379  ['group', $groups],
380  ['groups', $groups]
381  ]
382  ]
383  );
384  return $return;
385  }
386 
393  public function checkGroupname() {
394  // Groupnames can either be constrained by username validation, or be an
395  // email address.
396  if (Tilmeld::$config['email_usernames']
397  && $this->groupname === $this->email
398  ) {
399  return $this->checkEmail();
400  }
401  if (empty($this->groupname)) {
402  return ['result' => false, 'message' => 'Please specify a groupname.'];
403  }
404  if (Tilmeld::$config['max_username_length'] > 0
405  && strlen($this->groupname) > Tilmeld::$config['max_username_length']
406  ) {
407  return [
408  'result' => false,
409  'message' => 'Groupnames must not exceed '.
410  Tilmeld::$config['max_username_length'].' characters.'
411  ];
412  }
413  if (array_diff(
414  str_split($this->groupname),
415  str_split(Tilmeld::$config['valid_chars'])
416  )) {
417  return [
418  'result' => false,
419  'message' => Tilmeld::$config['valid_chars_notice']
420  ];
421  }
422  if (!preg_match(Tilmeld::$config['valid_regex'], $this->groupname)) {
423  return [
424  'result' => false,
425  'message' => Tilmeld::$config['valid_regex_notice']
426  ];
427  }
428  $selector = ['&',
429  'ilike' => [
430  'groupname',
431  str_replace(
432  ['\\', '%', '_'],
433  ['\\\\\\\\', '\%', '\_'],
434  $this->groupname
435  )
436  ]
437  ];
438  if (isset($this->guid)) {
439  $selector['!guid'] = $this->guid;
440  }
441  $test = Nymph::getEntity(
442  ['class' => '\Tilmeld\Entities\Group', 'skip_ac' => true],
443  $selector
444  );
445  if (isset($test->guid)) {
446  return ['result' => false, 'message' => 'That groupname is taken.'];
447  }
448 
449  return [
450  'result' => true,
451  'message' => (
452  isset($this->guid) ? 'Groupname is valid.' : 'Groupname is available!'
453  )
454  ];
455  }
456 
463  public function checkEmail() {
464  if ($this->email === '') {
465  return ['result' => true, 'message' => ''];
466  }
467  if (empty($this->email)) {
468  return ['result' => false, 'message' => 'Please specify a valid email.'];
469  }
470  if (!preg_match(
471  '/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i',
472  $this->email
473  )) {
474  return [
475  'result' => false,
476  'message' => 'Email must be a correctly formatted address.'
477  ];
478  }
479  $selector = ['&',
480  'ilike' => [
481  'email',
482  str_replace(['\\', '%', '_'], ['\\\\\\\\', '\%', '\_'], $this->email)
483  ]
484  ];
485  if (isset($this->guid)) {
486  $selector['!guid'] = $this->guid;
487  }
488  $test = Nymph::getEntity(
489  ['class' => '\Tilmeld\Entities\Group', 'skip_ac' => true],
490  $selector
491  );
492  if (isset($test->guid)) {
493  return [
494  'result' => false,
495  'message' => 'That email address is already registered.'
496  ];
497  }
498 
499  return [
500  'result' => true,
501  'message' => (
502  isset($this->guid) ? 'Email is valid.' : 'Email address is valid!'
503  )
504  ];
505  }
506 
507  public function save() {
508  if (!isset($this->groupname)) {
509  return false;
510  }
511 
512  // Formatting.
513  $this->groupname = trim($this->groupname);
514  $this->email = trim($this->email);
515  $this->name = trim($this->name);
516  $this->phone = trim($this->phone);
517 
518  // Verification.
519  $unCheck = $this->checkGroupname();
520  if (!$unCheck['result']) {
521  throw new \Tilmeld\Exceptions\BadUsernameException($unCheck['message']);
522  }
523  if (!(Tilmeld::$config['email_usernames']
524  && $this->groupname === $this->email)
525  ) {
526  $emCheck = $this->checkEmail();
527  if (!$emCheck['result']) {
528  throw new \Tilmeld\Exceptions\BadEmailException($emCheck['message']);
529  }
530  }
531 
532  // Validate group parent. Make sure it's not a descendant of this group.
533  if (isset($this->parent) &&
534  (
535  !isset($this->parent->guid) ||
536  $this->is($this->parent) ||
537  $this->parent->isDescendant($this)
538  )
539  ) {
540  throw new \Tilmeld\Exceptions\BadDataException(
541  'Group parent can\'t be itself or descendant of itself.'
542  );
543  }
544 
545  try {
546  Tilmeld::$config['validator_group']->assert($this->getValidatable());
547  // phpcs:ignore Generic.Files.LineLength.TooLong
548  } catch (\Respect\Validation\Exceptions\NestedValidationException $exception) {
549  throw new \Tilmeld\Exceptions\BadDataException(
550  $exception->getFullMessage()
551  );
552  }
553 
554  // Only one default primary group is allowed.
555  if ($this->defaultPrimary) {
556  $currentPrimary = Nymph::getEntity(
557  ['class' => '\Tilmeld\Entities\Group'],
558  ['&', 'equal' => ['defaultPrimary', true]]
559  );
560  if (isset($currentPrimary) && !$this->is($currentPrimary)) {
561  $currentPrimary->defaultPrimary = false;
562  if (!$currentPrimary->save()) {
563  // phpcs:ignore Generic.Files.LineLength.TooLong
564  throw new \Tilmeld\Exceptions\CouldNotChangeDefaultPrimaryGroupException(
565  "Could not change new user primary group from ".
566  "{$currentPrimary->groupname}."
567  );
568  }
569  }
570  }
571 
572  return parent::save();
573  }
574 
575  /*
576  * This should *never* be accessible on the client.
577  */
578  public function saveSkipAC() {
579  $this->skipAcWhenSaving = true;
580  return $this->save();
581  }
582 
583  public function tilmeldSaveSkipAC() {
584  if ($this->skipAcWhenSaving) {
585  $this->skipAcWhenSaving = false;
586  return true;
587  }
588  return false;
589  }
590 
591  public function delete() {
592  if (!Tilmeld::gatekeeper('tilmeld/admin')) {
593  return false;
594  }
595  $entities = Nymph::getEntities(
596  ['class' => '\Tilmeld\Entities\Group'],
597  ['&',
598  'ref' => ['parent', $this]
599  ]
600  );
601  foreach ($entities as $curGroup) {
602  if (!$curGroup->delete()) {
603  return false;
604  }
605  }
606  return parent::delete();
607  }
608 }
getUsers( $descendants=false, $limit=null, $offset=null)
Gets an array of users in the group.
Definition: Group.php:355
checkGroupname()
Check that a groupname is valid.
Definition: Group.php:393
getChildren()
Gets an array of the group's child groups.
Definition: Group.php:287
static gatekeeper($ability=null)
Check to see if the current user has an ability.
Definition: Tilmeld.php:58
checkEmail()
Check that an email is unique.
Definition: Group.php:463
getDescendants($andSelf=false)
Gets an array of the group's descendant groups.
Definition: Group.php:304
A group entity.
Definition: Group.php:49
__construct($id=0)
Load a group.
Definition: Group.php:97
Tilmeld main class.
Definition: Tilmeld.php:17
static getPrimaryGroups($search=null)
Get all the groups that can be assigned as primary groups.
Definition: Group.php:137
static getSecondaryGroups($search=null)
Get all the groups that can be assigned as secondary groups.
Definition: Group.php:150
isDescendant($group=null)
Check whether the group is a descendant of a group.
Definition: Group.php:262
updateDataProtection($user=null)
Update the data protection arrays for a user.
Definition: Group.php:233
getLevel()
Get the number of parents the group has.
Definition: Group.php:337