PluginProbe
Groups – Memberships and Access Control / 4.7.1
Groups – Memberships and Access Control v4.7.1
4.7.1 4.7.0 4.6.0 4.5.0 4.4.0 4.3.0 trunk 1.0.0-beta-1 1.0.0-beta-2 1.0.0-beta-3 1.0.0-beta-3b 1.0.0-beta-3c 1.0.0-beta-3d 1.1.4 1.1.5 1.10.0 1.10.1 1.10.2 1.10.3 1.11.0 1.11.1 1.11.2 1.11.3 1.12.0 1.13.0 All 131 releases
groups / lib / core / class-groups-group.php

class-groups-group.php in Groups – Memberships and Access Control 4.7.1, at lib/core/class-groups-group.php

1,228 lines 35.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * class-groups-group.php
4 *
5 * Copyright (c) "kento" Karim Rahimpur www.itthinx.com
6 *
7 * This code is released under the GNU General Public License.
8 * See COPYRIGHT.txt and LICENSE.txt.
9 *
10 * This code is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * This header and all notices must be kept intact.
16 *
17 * @author Karim Rahimpur
18 * @package groups
19 * @since groups 1.0.0
20 */
21
22 if ( !defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
27
28 require_once GROUPS_CORE_LIB . '/interface-i-capable.php';
29
30 /**
31 * Group OPM.
32 */
33 class Groups_Group implements I_Capable {
34
35 /**
36 * @var string cache group
37 */
38 const CACHE_GROUP = 'groups';
39
40 /**
41 * @var string key
42 *
43 * @deprecated since 4.3.0
44 */
45 const READ_GROUP_BY_ID = 'read_group_by_id';
46
47 /**
48 * @var string key
49 *
50 * @deprecated since 4.3.0
51 */
52 const READ_BY_NAME = 'read_by_name';
53
54 /**
55 * @var string key
56 */
57 const ID_MAP = 'map_group_by_id';
58
59 /**
60 * @var string key
61 */
62 const NAME_MAP = 'map_group_by_name';
63
64 /**
65 * @var int map limit
66 */
67 const MAX_MAP = 10000;
68
69 /**
70 * Lock timeout in microseconds.
71 *
72 * @var int
73 */
74 const LOCK_TIMEOUT = 30000000;
75
76 /**
77 * Lock name.
78 *
79 * @var string
80 */
81 const LOCK = 'groups_group_lock';
82
83 /**
84 * Mutex lock.
85 *
86 * @var Groups_Lock
87 */
88 private static $lock = null;
89
90 /**
91 * @var Object Persisted group.
92 *
93 * @access private - do not access this property directly, the visibility will be made private in the future
94 */
95 public $group = null;
96
97 /**
98 * Lock object.
99 *
100 * @throws Groups_Lock_Exception
101 *
102 * @return Groups_Lock
103 */
104 private static function get_lock() {
105 $lock = null;
106 if ( self::$lock !== null ) {
107 $lock = self::$lock;
108 } else {
109 $timeout = apply_filters( 'groups_group_lock_timeout', self::LOCK_TIMEOUT );
110 if ( is_numeric( $timeout ) ) {
111 $timeout = max( 0, intval( $timeout ) );
112 } else {
113 $timeout = null;
114 }
115 $lock = new Groups_Lock( self::LOCK, $timeout );
116 self::$lock = $lock;
117 }
118 return $lock;
119 }
120
121 /**
122 * Mutex locked.
123 *
124 * @return boolean
125 */
126 private static function is_locked() {
127 return self::$lock !== null && self::$lock->is_locked();
128 }
129
130 /**
131 * Mutex reader.
132 *
133 * @return boolean
134 */
135 private static function reader() {
136 $locked = false;
137 try {
138 $lock = self::get_lock();
139 $locked = $lock->reader();
140 } catch ( Groups_Lock_Exception $lex ) {
141 if ( defined( 'GROUPS_DEBUG' ) && GROUPS_DEBUG ) {
142 Groups_Log::log(
143 sprintf(
144 'Group read lock fail [%s] [%s]',
145 self::LOCK,
146 $lex->getMessage()
147 )
148 );
149 }
150 }
151 return $locked;
152 }
153
154 /**
155 * Mutex writer.
156 *
157 * @return boolean
158 */
159 private static function writer() {
160 $locked = false;
161 try {
162 $lock = self::get_lock();
163 $locked = $lock->writer();
164 } catch ( Groups_Lock_Exception $lex ) {
165 if ( defined( 'GROUPS_DEBUG' ) && GROUPS_DEBUG ) {
166 Groups_Log::log(
167 sprintf(
168 'Group write lock fail [%s] [%s]',
169 self::LOCK,
170 $lex->getMessage()
171 )
172 );
173 }
174 }
175 return $locked;
176 }
177
178 /**
179 * Mutex release.
180 *
181 * @return boolean
182 */
183 private static function release() {
184 $released = false;
185 if ( self::$lock !== null ) {
186 $released = self::$lock->release();
187 }
188 return $released;
189 }
190
191 /**
192 * Create by group id.
193 *
194 * Must have been persisted.
195 *
196 * @param int $group_id
197 */
198 public function __construct( $group_id ) {
199 $this->group = self::read( $group_id );
200 if ( $this->group === false ) {
201 $this->group = null;
202 }
203 }
204
205 /**
206 * Provides the object ID.
207 *
208 * @return int
209 */
210 public function get_id() {
211 return $this->get_group_id();
212 }
213
214 /**
215 * Provides the object ID.
216 *
217 * @return int
218 */
219 public function get_group_id() {
220 return $this->group_id; // @phpstan-ignore property.notFound
221 }
222
223 /**
224 * Provides the parent group's ID.
225 *
226 * @return int
227 */
228 public function get_parent_id() {
229 return $this->parent_id; // @phpstan-ignore property.notFound
230 }
231
232 /**
233 * Provides the creator's ID.
234 *
235 * @return int
236 */
237 public function get_creator_id() {
238 return $this->creator_id; // @phpstan-ignore property.notFound
239 }
240
241 /**
242 * Provides the date and time of creation.
243 *
244 * @return string
245 */
246 public function get_datetime() {
247 return $this->datetime; // @phpstan-ignore property.notFound
248 }
249
250 /**
251 * Provides the group's name.
252 *
253 * @return string
254 */
255 public function get_name() {
256 return $this->name; // @phpstan-ignore property.notFound
257 }
258
259 /**
260 * Provides the group's description.
261 *
262 * @return string
263 */
264 public function get_description() {
265 return $this->description; // @phpstan-ignore property.notFound
266 }
267
268 /**
269 * Provides the capabilities of the group.
270 *
271 * @return Groups_Capability[]
272 */
273 public function get_capabilities() {
274 return $this->capabilities; // @phpstan-ignore property.notFound
275 }
276
277 /**
278 * Provides the IDs of the capabilities of this group.
279 *
280 * @return int[]
281 */
282 public function get_capability_ids() {
283 return $this->capability_ids; // @phpstan-ignore property.notFound
284 }
285
286 /**
287 * Provides the capabilities of the group and of all its ancestors.
288 *
289 * @return Groups_Capability[]
290 */
291 public function get_capabilities_deep() {
292 return $this->capabilities_deep; // @phpstan-ignore property.notFound
293 }
294
295 /**
296 * Provides the IDs of the capabilities of this group and of all its ancestors.
297 *
298 * @return int[]
299 */
300 public function get_capability_ids_deep() {
301 return $this->capability_ids_deep; // @phpstan-ignore property.notFound
302 }
303
304 /**
305 * Provides the members of the group.
306 *
307 * @return Groups_User[]
308 */
309 public function get_users() {
310 return $this->users; // @phpstan-ignore property.notFound
311 }
312
313 /**
314 * Provides the user IDs of the members of this group.
315 *
316 * @return int[]
317 */
318 public function get_user_ids() {
319 return $this->user_ids; // @phpstan-ignore property.notFound
320 }
321
322 /**
323 * Retrieve a property by name.
324 *
325 * Possible properties:
326 * - group_id
327 * - parent_id
328 * - creator_id
329 * - datetime
330 * - name
331 * - description
332 * - capabilities, returns an array of Groups_Capability
333 * - users, returns an array of Groups_User
334 *
335 * @param string $name property's name
336 *
337 * @return mixed property value, will return null if property does not exist
338 */
339 public function __get( $name ) {
340 global $wpdb;
341 $result = null;
342 if ( $this->group !== null ) {
343 switch ( $name ) {
344 case 'group_id' :
345 case 'parent_id' :
346 case 'creator_id' :
347 case 'datetime' :
348 case 'name' :
349 case 'description' :
350 $result = $this->group->$name;
351 break;
352 case 'capability_ids' :
353 $result = array();
354 $group_capability_table = _groups_get_tablename( 'group_capability' );
355 $rows = $wpdb->get_results( $wpdb->prepare(
356 "SELECT capability_id FROM $group_capability_table WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
357 Groups_Utility::id( $this->group->group_id )
358 ) );
359 if ( $rows ) {
360 foreach ( $rows as $row ) {
361 $result[] = $row->capability_id;
362 }
363 }
364 break;
365 case 'capabilities' :
366 $result = array();
367 $capability_ids = $this->capability_ids; // @phpstan-ignore property.notFound
368 foreach ( $capability_ids as $capability_id ) {
369 $result[] = new Groups_Capability( $capability_id );
370 }
371 break;
372 case 'capabilities_deep' :
373 $result = array();
374 $capability_ids = $this->capability_ids_deep; // @phpstan-ignore property.notFound
375 foreach ( $capability_ids as $capability_id ) {
376 $result[] = new Groups_Capability( $capability_id );
377 }
378 break;
379 case 'capability_ids_deep' :
380 $capability_ids = array();
381 $group_table = _groups_get_tablename( 'group' );
382 $group_capability_table = _groups_get_tablename( "group_capability" );
383 // Find this group's and all its parent groups' capabilities.
384 $group_ids = array( Groups_Utility::id( $this->group->group_id ) );
385 $iterations = 0;
386 $old_group_ids_count = 0;
387 $all_groups = $wpdb->get_var( "SELECT COUNT(*) FROM $group_table" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
388 while( ( $iterations < $all_groups ) && ( count( $group_ids ) !== $old_group_ids_count ) ) {
389 $iterations++;
390 $old_group_ids_count = count( $group_ids );
391 $id_list = implode( ',', $group_ids );
392 $parent_group_ids = $wpdb->get_results(
393 "SELECT parent_id FROM $group_table WHERE parent_id IS NOT NULL AND group_id IN ($id_list)" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
394 );
395 if ( $parent_group_ids ) {
396 foreach ( $parent_group_ids as $parent_group_id ) {
397 $parent_group_id = Groups_Utility::id( $parent_group_id->parent_id );
398 if ( !in_array( $parent_group_id, $group_ids ) ) {
399 $group_ids[] = $parent_group_id;
400 }
401 }
402 }
403 }
404 if ( count( $group_ids ) > 0 ) {
405 $id_list = implode( ',', $group_ids );
406 $rows = $wpdb->get_results(
407 "SELECT DISTINCT capability_id FROM $group_capability_table WHERE group_id IN ($id_list)" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
408 );
409 if ( $rows ) {
410 foreach ( $rows as $row ) {
411 $capability_ids[] = $row->capability_id;
412 }
413 }
414 }
415 $result = $capability_ids;
416 break;
417 case 'users' :
418 $result = array();
419 $user_group_table = _groups_get_tablename( 'user_group' );
420 $users = $wpdb->get_results( $wpdb->prepare(
421 "SELECT $wpdb->users.* FROM $wpdb->users LEFT JOIN $user_group_table ON $wpdb->users.ID = $user_group_table.user_id WHERE $user_group_table.group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
422 Groups_Utility::id( $this->group->group_id )
423 ) );
424 if ( $users ) {
425 foreach ( $users as $user ) {
426 $groups_user = new Groups_User();
427 $groups_user->set_user( new WP_User( $user ) );
428 $result[] = $groups_user;
429 }
430 }
431 break;
432 case 'user_ids' :
433 $result = array();
434 $user_group_table = _groups_get_tablename( 'user_group' );
435 $user_ids = $wpdb->get_results( $wpdb->prepare(
436 "SELECT $wpdb->users.ID FROM $wpdb->users LEFT JOIN $user_group_table ON $wpdb->users.ID = $user_group_table.user_id WHERE $user_group_table.group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
437 Groups_Utility::id( $this->group->group_id )
438 ) );
439 if ( $user_ids ) {
440 foreach ( $user_ids as $user_id ) {
441 $result[] = $user_id->ID;
442 }
443 }
444 break;
445 }
446 }
447 return $result;
448 }
449
450 /**
451 * @see I_Capable::can()
452 */
453 public function can( $capability, $object = null, $args = null ) {
454
455 global $wpdb;
456 $result = false;
457
458 if ( $this->group !== null ) {
459
460 $group_table = _groups_get_tablename( 'group' );
461 $capability_table = _groups_get_tablename( 'capability' );
462 $group_capability_table = _groups_get_tablename( 'group_capability' );
463
464 // determine capability id
465 $capability_id = null;
466 if ( is_numeric( $capability ) ) {
467 $capability_id = Groups_Utility::id( $capability );
468 } else if ( is_string( $capability ) ) {
469 $capability_id = $wpdb->get_var( $wpdb->prepare(
470 "SELECT capability_id FROM $capability_table WHERE capability = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
471 $capability
472 ) );
473 }
474
475 if ( $capability_id !== null ) {
476 // check if the group itself can
477 $result = is_object( $this->group ) ? ( Groups_Group_Capability::read( $this->group->group_id, $capability_id ) !== false ) : null;
478 if ( !$result ) {
479 // find all parent groups and include in the group's
480 // upward hierarchy to see if any of these can
481 $group_ids = is_object( $this->group ) ? array( $this->group->group_id ) : array();
482 $iterations = 0;
483 $old_group_ids_count = 0;
484 $all_groups = $wpdb->get_var( "SELECT COUNT(*) FROM $group_table" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
485 while( ( $iterations < $all_groups ) && ( count( $group_ids ) !== $old_group_ids_count ) ) {
486 $iterations++;
487 $old_group_ids_count = count( $group_ids );
488 $id_list = implode( ',', $group_ids );
489 $parent_group_ids = $wpdb->get_results(
490 "SELECT parent_id FROM $group_table WHERE parent_id IS NOT NULL AND group_id IN ($id_list)" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
491 );
492 if ( $parent_group_ids ) {
493 foreach ( $parent_group_ids as $parent_group_id ) {
494 $parent_group_id = Groups_Utility::id( $parent_group_id->parent_id );
495 if ( !in_array( $parent_group_id, $group_ids ) ) {
496 $group_ids[] = $parent_group_id;
497 }
498 }
499 }
500 }
501 if ( count( $group_ids ) > 0 ) {
502 $id_list = implode( ',', $group_ids );
503 $rows = $wpdb->get_results( $wpdb->prepare(
504 "SELECT capability_id FROM $group_capability_table WHERE capability_id = %d AND group_id IN ($id_list)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
505 Groups_Utility::id( $capability_id )
506 ) );
507
508 if ( count( $rows ) > 0 ) {
509 $result = true;
510 }
511 }
512 }
513 }
514 }
515 /**
516 * Filter whether the group has the capability.
517 *
518 * @since 3.0.0 $object
519 * @since 3.0.0 $args
520 *
521 * @param boolean $result
522 * @param Groups_Group $group
523 * @param string $capability
524 * @param mixed $object
525 * @param mixed $args
526 *
527 * @return boolean
528 */
529 $result = apply_filters_ref_array( 'groups_group_can', array( $result, &$this, $capability, $object, $args ) );
530 return $result;
531 }
532
533 /**
534 * Check if a group with the given ID exists.
535 *
536 * @since 2.18.0
537 *
538 * @param int $group_id
539 *
540 * @return boolean
541 */
542 public static function exists( $group_id ) {
543 $exists = false;
544 if ( !empty( $group_id ) && is_numeric( $group_id ) ) {
545 $group_id = Groups_Utility::id( $group_id );
546 if ( $group_id !== false ) {
547 $exists = self::read( $group_id ) !== false;
548 }
549 }
550 return $exists;
551 }
552
553 /**
554 * Persist a group.
555 *
556 * Parameters:
557 * - name (required) - the group's name
558 * - creator_id (optional) - defaults to the current user's id
559 * - datetime (optional) - defaults to now
560 * - description (optional)
561 * - parent_id (optional)
562 *
563 * @param array $map attributes
564 *
565 * @return int group_id on success, otherwise false
566 */
567 public static function create( $map ) {
568
569 global $wpdb;
570
571 $result = false;
572 $error = false;
573
574 $name = isset( $map['name'] ) ? $map['name'] : null;
575 $creator_id = isset( $map['creator_id'] ) ? $map['creator_id'] : null;
576 $datetime = isset( $map['datetime'] ) ? $map['datetime'] : null;
577 $description = isset( $map['description'] ) ? $map['description'] : null;
578 $parent_id = isset( $map['parent_id'] ) ? $map['parent_id'] : null;
579
580 if ( !empty( $name ) ) {
581
582 self::writer();
583
584 $group_table = _groups_get_tablename( 'group' );
585
586 $data = array( 'name' => $name );
587 $formats = array( '%s' );
588 if ( $creator_id === null ) {
589 $creator_id = get_current_user_id();
590 }
591 if ( $creator_id !== null ) {
592 $data['creator_id'] = Groups_Utility::id( $creator_id );
593 $formats[] = '%d';
594 }
595 if ( $datetime === null ) {
596 $datetime = date( 'Y-m-d H:i:s', time() ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
597 }
598 if ( !empty( $datetime ) ) {
599 $data['datetime'] = $datetime;
600 $formats[] = '%s';
601 }
602 if ( !empty( $description ) ) {
603 $data['description'] = $description;
604 $formats[] = '%s';
605 }
606 if ( !empty( $parent_id ) ) {
607 $parent_id = Groups_Utility::id( $parent_id );
608 if ( $parent_id !== false ) {
609 // only allow to set an existing parent group (that is from the same blog)
610 $parent_group_id = $wpdb->get_var(
611 $wpdb->prepare(
612 "SELECT group_id FROM $group_table WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
613 Groups_Utility::id( $parent_id )
614 )
615 );
616 if ( $parent_group_id !== null ) {
617 $parent_group_id = intval( $parent_group_id );
618 }
619 if (
620 $parent_group_id !== null &&
621 $parent_group_id === $parent_id
622 ) {
623 $data['parent_id'] = $parent_id;
624 $formats[] = '%d';
625 } else {
626 $error = true;
627 }
628 }
629 }
630 // no duplicate names
631 $duplicate = Groups_Group::read_by_name( $name );
632 if ( $duplicate ) {
633 $error = true;
634 }
635 if ( !$error ) {
636 if ( $wpdb->insert( $group_table, $data, $formats ) ) {
637 if ( $result = $wpdb->get_var( "SELECT LAST_INSERT_ID()" ) ) {
638 // purge maps to force update after creating this group
639 Groups_Cache::delete( self::ID_MAP, self::CACHE_GROUP );
640 Groups_Cache::delete( self::NAME_MAP, self::CACHE_GROUP );
641 }
642 }
643 }
644
645 self::release();
646
647 if ( $result !== false ) {
648 do_action( 'groups_created_group', $result );
649 }
650
651 }
652 return $result;
653 }
654
655 /**
656 * Retrieve a group.
657 *
658 * @param int $group_id group's id
659 *
660 * @return object upon success, otherwise false
661 */
662 public static function read( $group_id ) {
663 global $wpdb;
664
665 $group_id = Groups_Utility::id( $group_id );
666 if ( $group_id === false ) {
667 return false;
668 }
669
670 $result = false;
671
672 $max_map = apply_filters( 'groups_group_read_max_map', self::MAX_MAP );
673 if ( !is_numeric( $max_map ) ) {
674 $max_map = self::MAX_MAP;
675 } else {
676 $max_map = max( 0, intval( $max_map ) );
677 }
678
679 $is_locked = self::is_locked();
680 if ( !$is_locked ) {
681 self::writer();
682 }
683
684 $cached = Groups_Cache::get( self::ID_MAP, self::CACHE_GROUP );
685 if ( $cached !== null ) {
686 $map = $cached->get_value();
687 $result = $map[$group_id] ?? false;
688 if ( $result === false && count( $map ) >= $max_map ) {
689 // requested is not in map
690 $group_table = _groups_get_tablename( 'group' );
691 $group = $wpdb->get_row( $wpdb->prepare(
692 "SELECT * FROM $group_table WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
693 $group_id
694 ) );
695 if ( isset( $group->group_id ) ) {
696 $result = $group;
697 // push requested to map
698 $map = array( $group->group_id => $group ) + $map; // numerical key is automatically cast to int
699 array_pop( $map );
700 Groups_Cache::set( self::ID_MAP, $map, self::CACHE_GROUP );
701 }
702 }
703 } else {
704 // map not cached, prime the maps
705 $map = array();
706 $name_map = array();
707 $group_table = _groups_get_tablename( 'group' );
708 $query = $wpdb->prepare( "SELECT * FROM $group_table LIMIT %d", $max_map ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
709 $groups = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
710 if ( is_array( $groups ) ) {
711 foreach ( $groups as $group ) {
712 $map[$group->group_id] = $group; // numerical key is automatically cast to int
713 $name_map[$group->name] = $group;
714 }
715 }
716 if ( isset( $map[$group_id] ) ) {
717 $result = $map[$group_id];
718 } else if ( count( $map ) >= $max_map ) {
719 // requested is not in map
720 $group = $wpdb->get_row( $wpdb->prepare(
721 "SELECT * FROM $group_table WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
722 $group_id
723 ) );
724 if ( isset( $group->group_id ) ) {
725 $result = $group;
726 // push requested to maps
727 if ( !isset( $map[intval( $group->group_id )] ) ) { // double-check
728 $map = array( $group->group_id => $group ) + $map; // numerical key is automatically cast to int
729 array_pop( $map );
730 }
731 if ( !isset( $name_map[$group->name] ) ) {
732 $name_map = array( $group->name => $group ) + $name_map;
733 array_pop( $name_map );
734 }
735 }
736 }
737 Groups_Cache::set( self::ID_MAP, $map, self::CACHE_GROUP );
738 Groups_Cache::set( self::NAME_MAP, $name_map, self::CACHE_GROUP );
739 }
740
741 if ( !$is_locked ) {
742 self::release();
743 }
744
745 return $result;
746 }
747
748 /**
749 * Retrieve a group by name.
750 *
751 * @param string $name the group's name
752 *
753 * @return object upon success, otherwise false
754 */
755 public static function read_by_name( $name ) {
756 global $wpdb;
757
758 $result = false;
759
760 $max_map = apply_filters( 'groups_group_read_by_name_max_map', self::MAX_MAP );
761 if ( !is_numeric( $max_map ) ) {
762 $max_map = self::MAX_MAP;
763 } else {
764 $max_map = max( 0, intval( $max_map ) );
765 }
766
767 $is_locked = self::is_locked();
768 if ( !$is_locked ) {
769 self::writer();
770 }
771
772 $cached = Groups_Cache::get( self::NAME_MAP, self::CACHE_GROUP );
773 if ( $cached !== null ) {
774 $name_map = $cached->get_value();
775 $result = $name_map[$name] ?? false; // will be false if case does not match
776 if ( $result === false ) {
777 // requested not in map
778 $group_table = _groups_get_tablename( 'group' );
779 $group = $wpdb->get_row( $wpdb->prepare(
780 "SELECT * FROM $group_table WHERE name = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
781 $name
782 ) );
783 if ( isset( $group->group_id ) ) {
784 $result = $group;
785 // push requested to map
786 if ( !isset( $name_map[$group->name] ) ) {
787 $name_map = array( $group->name => $group ) + $name_map;
788 if ( count( $name_map ) > $max_map ) {
789 array_pop( $name_map );
790 }
791 Groups_Cache::set( self::NAME_MAP, $name_map, self::CACHE_GROUP );
792 }
793 // where case insensitive collation yields result also push to map using original $name
794 // so it can also be retrieved from cached map by $name as key
795 if ( !isset( $name_map[$name] ) ) {
796 $name_map = array( $name => $group ) + $name_map;
797 if ( count( $name_map ) > $max_map ) {
798 array_pop( $name_map );
799 }
800 Groups_Cache::set( self::NAME_MAP, $name_map, self::CACHE_GROUP );
801 }
802 }
803 }
804 } else {
805 $map = array();
806 $name_map = array();
807 $group_table = _groups_get_tablename( 'group' );
808 $query = $wpdb->prepare( "SELECT * FROM $group_table LIMIT %d", $max_map ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
809 $groups = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
810 if ( is_array( $groups ) ) {
811 foreach ( $groups as $group ) {
812 $map[$group->group_id] = $group; // numerical key is automatically cast to int
813 $name_map[$group->name] = $group;
814 }
815 }
816 if ( isset( $name_map[$name] ) ) {
817 $result = $name_map[$name];
818 } else {
819 $group = $wpdb->get_row( $wpdb->prepare(
820 "SELECT * FROM $group_table WHERE name = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
821 $name
822 ) );
823 if ( isset( $group->group_id ) ) {
824 $result = $group;
825 if ( !isset( $map[$group->group_id] ) ) {
826 $map = array( $group->group_id => $group ) + $map; // numerical key is automatically cast to int
827 if ( count( $name_map ) > $max_map ) {
828 array_pop( $map );
829 }
830 }
831 if ( !isset( $name_map[$group->name] ) ) {
832 $name_map = array( $group->name => $group ) + $name_map;
833 array_pop( $name_map );
834 }
835 if ( !isset( $name_map[$name] ) ) {
836 $name_map = array( $name => $group ) + $name_map;
837 if ( count( $name_map ) > $max_map ) {
838 array_pop( $name_map );
839 }
840 }
841 }
842 }
843 Groups_Cache::set( self::ID_MAP, $map, self::CACHE_GROUP );
844 Groups_Cache::set( self::NAME_MAP, $name_map, self::CACHE_GROUP );
845 }
846
847 if ( !$is_locked ) {
848 self::release();
849 }
850
851 return $result;
852 }
853
854 /**
855 * Update group.
856 *
857 * @param array $map group attribute, must contain group_id
858 *
859 * @return int group_id on success, otherwise false
860 */
861 public static function update( $map ) {
862
863 global $wpdb;
864
865 $result = false;
866
867 $group_id = isset( $map['group_id'] ) ? $map['group_id'] : null;
868 $name = isset( $map['name'] ) ? $map['name'] : null;
869 $description = isset( $map['description'] ) ? $map['description'] : null;
870 $parent_id = isset( $map['parent_id'] ) ? $map['parent_id'] : null;
871
872 if ( isset( $group_id ) && !empty( $name ) ) {
873
874 self::writer();
875
876 $group_table = _groups_get_tablename( 'group' );
877 if ( !isset( $description ) || ( $description === null ) ) {
878 $description = '';
879 }
880 $wpdb->query( $wpdb->prepare(
881 "UPDATE $group_table SET name = %s, description = %s WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
882 $name,
883 $description,
884 Groups_Utility::id( $group_id )
885 ) );
886 if ( empty( $parent_id ) ) {
887 $wpdb->query( $wpdb->prepare(
888 "UPDATE $group_table SET parent_id = NULL WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
889 Groups_Utility::id( $group_id )
890 ) );
891 } else {
892 // Prohibit circular dependencies:
893 // This group cannot have a parent that is its successor
894 // at any level in its successor hierarchy.
895 // S(g) : successor of group g
896 // S*(g) : successors of group g, any level deep
897 // P(g) : parent of g
898 // ---
899 // It must hold: !( P(g) in S*(g) )
900
901 // Find all successors of this group
902 $groups = $wpdb->get_var( "SELECT COUNT(*) FROM $group_table" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
903 if ( $groups !== null ) {
904 $group_ids = array();
905 $group_ids[] = Groups_Utility::id( $group_id );
906 $iterations = 0;
907 $old_group_ids_count = 0;
908 while( ( $iterations < $groups ) && ( count( $group_ids ) > 0 ) && ( count( $group_ids ) !== $old_group_ids_count ) ) {
909
910 $iterations++;
911 $old_group_ids_count = count( $group_ids );
912
913 $id_list = implode( ',', $group_ids );
914 // We can trust ourselves here, no need to use prepare()
915 // but careful if this query is modified!
916 $successor_group_ids = $wpdb->get_results(
917 "SELECT group_id FROM $group_table WHERE parent_id IS NOT NULL AND parent_id IN ($id_list)" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
918 );
919 if ( $successor_group_ids ) {
920 foreach ( $successor_group_ids as $successor_group_id ) {
921 $successor_group_id = Groups_Utility::id( $successor_group_id->group_id );
922 if ( !in_array( $successor_group_id, $group_ids ) ) {
923 $group_ids[] = $successor_group_id;
924 }
925 }
926 }
927 }
928 // only add if condition holds
929 if ( !in_array( Groups_Utility::id( $parent_id ), $group_ids ) ) {
930 $wpdb->query( $wpdb->prepare(
931 "UPDATE $group_table SET parent_id = %d WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
932 Groups_Utility::id( $parent_id),
933 Groups_Utility::id( $group_id )
934 ) );
935 }
936 }
937 }
938 $result = $group_id;
939
940 if ( !empty( $group_id ) || !empty( $name ) ) { // @phpstan-ignore empty.variable
941 Groups_Cache::delete( self::ID_MAP, self::CACHE_GROUP );
942 Groups_Cache::delete( self::NAME_MAP, self::CACHE_GROUP );
943 }
944
945 self::release();
946
947 do_action( 'groups_updated_group', $result );
948 }
949 return $result;
950 }
951
952 /**
953 * Remove group and its relations.
954 *
955 * @param int $group_id
956 *
957 * @return int group_id if successful, false otherwise
958 */
959 public static function delete( $group_id ) {
960
961 global $wpdb;
962 $result = false;
963
964 self::writer();
965
966 if ( $group = self::read( $group_id ) ) {
967
968 // delete group-capabilities
969 $group_capability_table = _groups_get_tablename( 'group_capability' );
970 $wpdb->query( $wpdb->prepare(
971 "DELETE FROM $group_capability_table WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
972 Groups_Utility::id( $group->group_id )
973 ) );
974
975 // delete group-users
976 $user_group_table = _groups_get_tablename( 'user_group' );
977 $wpdb->query( $wpdb->prepare(
978 "DELETE FROM $user_group_table WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
979 $group->group_id
980 ) );
981
982 // set parent_id to null where this group is parent
983 $group_table = _groups_get_tablename( 'group' );
984 $wpdb->query( $wpdb->prepare(
985 "UPDATE $group_table SET parent_id = NULL WHERE parent_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
986 $group->group_id
987 ) );
988
989 // delete group
990 if ( $wpdb->query( $wpdb->prepare(
991 "DELETE FROM $group_table WHERE group_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
992 $group->group_id
993 ) ) ) {
994 $result = $group->group_id;
995 if ( !empty( $group->group_id ) || !empty( $group->name ) ) {
996 Groups_Cache::delete( self::ID_MAP, self::CACHE_GROUP );
997 Groups_Cache::delete( self::NAME_MAP, self::CACHE_GROUP );
998 }
999 }
1000 }
1001
1002 self::release();
1003
1004 if ( $result !== false ) {
1005 do_action( 'groups_deleted_group', $result );
1006 }
1007
1008 return $result;
1009 }
1010
1011 /**
1012 * Returns an array of group IDs.
1013 *
1014 * If no arguments are passed, IDs for all existing groups are returned.
1015 *
1016 * @param array $args
1017 * - ['order_by'] string a Groups_Group property
1018 * - ['order'] string ASC or DESC. Only applied if 'order_by' is set.
1019 * - ['parent_id'] int retrieve groups whose parent is indicated by this ID
1020 * - ['include'] array|string with one or more IDs of groups to include, separated by comma
1021 * - ['include_by_name'] array|string with one ore more group names of groups to include, separated by comma
1022 * - ['exclude'] array|string with one or more IDs of groups to exclude, separated by comma
1023 * - ['exclude_by_name'] array|string with one ore more group names of groups to exclude, separated by comma
1024 *
1025 * @return array of int with group IDs
1026 *
1027 * @since groups 1.4.9
1028 */
1029 public static function get_group_ids( $args = array() ) {
1030 $result = array();
1031 $args['fields'] = 'group_id';
1032 $groups = self::get_groups( $args );
1033 if ( sizeof( $groups ) > 0 ) {
1034 foreach ( $groups as $group ) {
1035 $result[] = $group->group_id;
1036 }
1037 }
1038 return $result;
1039 }
1040
1041 /**
1042 * Returns an array of database results by querying the group table.
1043 *
1044 * @param Array $args
1045 * - ['fields'] string with fields to get separated by comma. If empty then get all fields.
1046 * - ['order_by'] string a Groups_Group property
1047 * - ['order'] string ASC or DESC. Only applied if 'order_by' is set.
1048 * - ['parent_id'] int retrieve groups whose parent is indicated by this ID
1049 * - ['include'] array|string with one or more IDs of groups to include, separated by comma
1050 * - ['include_by_name'] array|string with one ore more group names of groups to include, separated by comma
1051 * - ['exclude'] array|string with one or more IDs of groups to exclude, separated by comma
1052 * - ['exclude_by_name'] array|string with one ore more group names of groups to exclude, separated by comma
1053 *
1054 * @return array of object with query rows
1055 *
1056 * @since groups 1.4.9
1057 */
1058 public static function get_groups( $args = array() ) {
1059 global $wpdb;
1060
1061 $fields = isset( $args['fields'] ) ? $args['fields'] : null;
1062 $order = isset( $args['order'] ) ? $args['order'] : null;
1063 $order_by = isset( $args['order_by'] ) ? $args['order_by'] : null;
1064 $parent_id = isset( $args['parent_id'] ) ? $args['parent_id'] : null;
1065 $include = isset( $args['include'] ) ? $args['include'] : null;
1066 $include_by_name = isset( $args['include_by_name'] ) ? $args['include_by_name'] : null;
1067 $exclude = isset( $args['exclude'] ) ? $args['exclude'] : null;
1068 $exclude_by_name = isset( $args['exclude_by_name'] ) ? $args['exclude_by_name'] : null;
1069
1070 if ( !isset( $fields ) ) {
1071 $fields = '*';
1072 } else {
1073 $array_fields = explode( ',', sanitize_text_field( $fields ) );
1074 $fields = '';
1075 foreach ( $array_fields as $field ) {
1076 switch ( trim( $field ) ) {
1077 case 'group_id' :
1078 case 'parent_id' :
1079 case 'creator_id' :
1080 case 'datetime' :
1081 case 'name' :
1082 case 'description' :
1083 $fields .= ',' . trim( $field );
1084 break;
1085 }
1086 }
1087 if ( strlen( $fields ) > 0 ) {
1088 $fields = substr( $fields, 1 );
1089 }
1090 }
1091
1092 if ( !isset( $order ) ) {
1093 $order = '';
1094 } else {
1095 $order = strtoupper( sanitize_text_field( trim( $order ) ) );
1096 switch ( $order ) {
1097 case 'ASC' :
1098 case 'DESC' :
1099 break;
1100 default :
1101 $order = 'ASC';
1102 }
1103 }
1104
1105 if ( !isset( $order_by ) ) {
1106 $order_by = '';
1107 } else {
1108 $order_by = sanitize_text_field( $order_by );
1109 switch ( trim( $order_by ) ) {
1110 case 'group_id' :
1111 case 'parent_id' :
1112 case 'creator_id' :
1113 case 'datetime' :
1114 case 'name' :
1115 case 'description' :
1116 $order_by = " ORDER BY $order_by $order "; // Watch out! This is unescaped but safe within this switch.
1117 break;
1118 default :
1119 $order_by = '';
1120 break;
1121 }
1122 }
1123
1124 $where = '';
1125 if ( isset( $parent_id ) ) {
1126 $parent_id = sanitize_text_field( $parent_id );
1127 if ( is_numeric ( $parent_id ) ) {
1128 $where .= $wpdb->prepare( " WHERE parent_id=%s ", array( $parent_id ) );
1129 }
1130 }
1131
1132 //
1133 // include by group ID
1134 //
1135 $where_include = '';
1136 $include = !empty( $include ) ? $include : null;
1137 if ( !empty( $include ) && !is_array( $include ) && is_string( $include ) ) {
1138 $include = explode( ',', $include );
1139 }
1140 if ( $include !== null && count( $include ) > 0 ) {
1141 $include = implode( ',', array_map( 'intval', array_map( 'trim', $include ) ) );
1142 if ( strlen( $include ) > 0 ) {
1143 $where_include = " group_id IN ($include) ";
1144 }
1145 }
1146
1147 //
1148 // include by group name
1149 //
1150 $where_include_by_name = '';
1151 $include_by_name = !empty( $include_by_name ) ? $include_by_name : null;
1152 if ( !empty( $include_by_name ) && !is_array( $include_by_name ) && is_string( $include_by_name ) ) {
1153 $include_by_name = explode( ',', $include_by_name );
1154 }
1155 if ( $include_by_name !== null && count( $include_by_name ) > 0 ) {
1156 $include_by_name = "'" . implode( "','", array_map( 'esc_sql', array_map( 'trim', $include_by_name ) ) ) . "'";
1157 if ( strlen( $include_by_name ) > 0 ) {
1158 $where_include_by_name = " name IN ($include_by_name) ";
1159 }
1160 }
1161
1162 // adding includes ...
1163 if ( ( $where_include !== '' ) || ( $where_include_by_name !== '' ) ) {
1164 if ( $where == '' ) {
1165 $where .= " WHERE ";
1166 } else {
1167 $where .= " AND ";
1168 }
1169 }
1170 if ( ( $where_include !== "" ) && ( $where_include_by_name !== "" ) ) {
1171 $where .= "(";
1172 }
1173 if ( $where_include !== "" ) {
1174 $where .= $where_include;
1175 }
1176 if ( ( $where_include !== "" ) && ( $where_include_by_name !== "" ) ) {
1177 $where .= " OR ";
1178 }
1179 if ( $where_include_by_name !== "" ) {
1180 $where .= $where_include_by_name;
1181 }
1182 if ( ( $where_include !== "" ) && ( $where_include_by_name !== "" ) ) {
1183 $where .= ")";
1184 }
1185
1186 //
1187 // exclude
1188 //
1189 $exclude = !empty( $exclude ) ? $exclude : null;
1190 if ( !empty( $exclude ) && !is_array( $exclude ) && is_string( $exclude ) ) {
1191 $exclude = explode( ',', $exclude );
1192 }
1193 if ( $exclude !== null && count( $exclude ) > 0 ) {
1194 $exclude = implode( ',', array_map( 'intval', array_map( 'trim', $exclude ) ) );
1195 if ( strlen( $exclude ) > 0 ) {
1196 if ( empty( $where ) ) {
1197 $where = " WHERE group_id NOT IN ($exclude) ";
1198 } else {
1199 $where .= " AND group_id NOT IN ($exclude) ";
1200 }
1201 }
1202 }
1203
1204 //
1205 // exclude by group name
1206 //
1207 $exclude_by_name = !empty( $exclude_by_name ) ? $exclude_by_name : null;
1208 if ( !empty( $exclude_by_name ) && !is_array( $exclude_by_name ) && is_string( $exclude_by_name ) ) {
1209 $exclude_by_name = explode( ',', $exclude_by_name );
1210 }
1211 if ( $exclude_by_name !== null && count( $exclude_by_name ) > 0 ) {
1212 $exclude_by_name = "'" . implode( "','", array_map( 'esc_sql', array_map( 'trim', $exclude_by_name ) ) ) . "'";
1213 if ( strlen( $exclude_by_name ) > 0 ) {
1214 if ( empty( $where ) ) {
1215 $where = " WHERE name NOT IN ($exclude_by_name) ";
1216 } else {
1217 $where .= " AND name NOT IN ($exclude_by_name) ";
1218 }
1219 }
1220 }
1221
1222 $groups_table = _groups_get_tablename( 'group' );
1223 $groups = $wpdb->get_results( "SELECT $fields FROM $groups_table $where $order_by" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1224
1225 return $groups;
1226 }
1227 }
1228