Merge pull request #3342 from systopia/CRM-14741
[civicrm-core.git] / CRM / Contact / BAO / GroupNesting.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright U.S. PIRG Education Fund (c) 2007 |
7 | Licensed to CiviCRM under the Academic Free License version 3.0. |
8 +--------------------------------------------------------------------+
9 | This file is a part of CiviCRM. |
10 | |
11 | CiviCRM is free software; you can copy, modify, and distribute it |
12 | under the terms of the GNU Affero General Public License |
13 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | |
15 | CiviCRM is distributed in the hope that it will be useful, but |
16 | WITHOUT ANY WARRANTY; without even the implied warranty of |
17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
18 | See the GNU Affero General Public License for more details. |
19 | |
20 | You should have received a copy of the GNU Affero General Public |
21 | License and the CiviCRM Licensing Exception along |
22 | with this program; if not, contact CiviCRM LLC |
23 | at info[AT]civicrm[DOT]org. If you have questions about the |
24 | GNU Affero General Public License or the licensing of CiviCRM, |
25 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
26 +--------------------------------------------------------------------+
27 */
28
29 /**
30 *
31 * @package CRM
32 * @copyright U.S. PIRG 2007
33 * $Id$
34 *
35 */
36 class CRM_Contact_BAO_GroupNesting extends CRM_Contact_DAO_GroupNesting implements Iterator {
37
38 static $_sortOrder = 'ASC';
39
40 private $_current;
41
42 private $_parentStack = array();
43
44 private $_lastParentlessGroup;
45
46 private $_styleLabels;
47
48 private $_styleIndent;
49
50 private $_alreadyStyled = FALSE;
51
52 /**
53 * class constructor
54 */
55 function __construct($styleLabels = FALSE, $styleIndent = "&nbsp;--&nbsp;") {
56 parent::__construct();
57 $this->_styleLabels = $styleLabels;
58 $this->_styleIndent = $styleIndent;
59 }
60
61 /**
62 * @param $sortOrder
63 */
64 function setSortOrder($sortOrder) {
65 switch ($sortOrder) {
66 case 'ASC':
67 case 'DESC':
68 if ($sortOrder != self::$_sortOrder) {
69 self::$_sortOrder = $sortOrder;
70 $this->rewind();
71 }
72 break;
73
74 default:
75 // spit out some error, someday
76 }
77 }
78
79 /**
80 * @return string
81 */
82 function getSortOrder() {
83 return self::$_sortOrder;
84 }
85
86 /**
87 * @return int
88 */
89 function getCurrentNestingLevel() {
90 return count($this->_parentStack);
91 }
92
93 /**
94 * Go back to the first element in the group nesting graph,
95 * which is the first group (according to _sortOrder) that
96 * has no parent groups
97 */
98 function rewind() {
99 $this->_parentStack = array();
100 // calling _getNextParentlessGroup w/ no arguments
101 // makes it return the first parentless group
102 $firstGroup = $this->_getNextParentlessGroup();
103 $this->_current = $firstGroup;
104 $this->_lastParentlessGroup = $firstGroup;
105 $this->_alreadyStyled = FALSE;
106 }
107
108 function current() {
109 if ($this->_styleLabels &&
110 $this->valid() &&
111 !$this->_alreadyStyled
112 ) {
113 $styledGroup = clone($this->_current);
114 $nestingLevel = $this->getCurrentNestingLevel();
115 $indent = '';
116 while ($nestingLevel--) {
117 $indent .= $this->_styleIndent;
118 }
119 $styledGroup->title = $indent . $styledGroup->title;
120
121 $this->_current = &$styledGroup;
122 $this->_alreadyStyled = TRUE;
123 }
124 return $this->_current;
125 }
126
127 /**
128 * @return string
129 */
130 function key() {
131 $group = &$this->_current;
132 $ids = array();
133 foreach ($this->_parentStack as $parentGroup) {
134 $ids[] = $parentGroup->id;
135 }
136 $key = implode('-', $ids);
137 if (strlen($key) > 0) {
138 $key .= '-';
139 }
140 $key .= $group->id;
141 return $key;
142 }
143
144 /**
145 * @return CRM_Contact_BAO_Group|null
146 */
147 function next() {
148 $currentGroup = &$this->_current;
149 $childGroup = $this->_getNextChildGroup($currentGroup);
150 if ($childGroup) {
151 $nextGroup = &$childGroup;
152 $this->_parentStack[] = &$this->_current;
153 }
154 else {
155 $nextGroup = $this->_getNextSiblingGroup($currentGroup);
156 if (!$nextGroup) {
157 // no sibling, find an ancestor w/ a sibling
158 for (;; ) {
159 // since we pop this array everytime, we should be
160 // reasonably safe from infinite loops, I think :)
161 $ancestor = array_pop($this->_parentStack);
162 $this->_current = &$ancestor;
163 if ($ancestor == NULL) {
164 break;
165 }
166 $nextGroup = $this->_getNextSiblingGroup($ancestor);
167 if ($nextGroup) {
168 break;
169 }
170 }
171 }
172 }
173 $this->_current = &$nextGroup;
174 $this->_alreadyStyled = FALSE;
175 return $nextGroup;
176 }
177
178 /**
179 * @return bool
180 */
181 function valid() {
182 if ($this->_current) {
183 return TRUE;
184 }
185 else {
186 return FALSE;
187 }
188 }
189
190 /**
191 * @param null $group
192 *
193 * @return CRM_Contact_BAO_Group|null
194 */
195 function _getNextParentlessGroup(&$group = NULL) {
196 $lastParentlessGroup = $this->_lastParentlessGroup;
197 $nextGroup = new CRM_Contact_BAO_Group();
198 $nextGroup->order_by = 'title ' . self::$_sortOrder;
199 $nextGroup->find();
200 if ($group == NULL) {
201 $sawLast = TRUE;
202 }
203 else {
204 $sawLast = FALSE;
205 }
206 while ($nextGroup->fetch()) {
207 if (!self::hasParentGroups($nextGroup->id) && $sawLast) {
208 return $nextGroup;
209 }
210 elseif ($lastParentlessGroup->id == $nextGroup->id) {
211 $sawLast = TRUE;
212 }
213 }
214 return NULL;
215 }
216
217 /**
218 * @param $parentGroup
219 * @param null $group
220 *
221 * @return CRM_Contact_BAO_Group|null
222 */
223 function _getNextChildGroup(&$parentGroup, &$group = NULL) {
224 $children = self::getChildGroupIds($parentGroup->id);
225 if (count($children) > 0) {
226 // we have child groups, so get the first one based on _sortOrder
227 $childGroup = new CRM_Contact_BAO_Group();
228 $cgQuery = "SELECT * FROM civicrm_group WHERE id IN (" . implode(',', $children) . ") ORDER BY title " . self::$_sortOrder;
229 $childGroup->query($cgQuery);
230 $currentGroup = &$this->_current;
231 if ($group == NULL) {
232 $sawLast = TRUE;
233 }
234 else {
235 $sawLast = FALSE;
236 }
237 while ($childGroup->fetch()) {
238 if ($sawLast) {
239 return $childGroup;
240 }
241 elseif ($currentGroup->id === $childGroup->id) {
242 $sawLast = TRUE;
243 }
244 }
245 }
246 return NULL;
247 }
248
249 /**
250 * @param $group
251 *
252 * @return CRM_Contact_BAO_Group|null
253 */
254 function _getNextSiblingGroup(&$group) {
255 $parentGroup = end($this->_parentStack);
256 if ($parentGroup) {
257 $nextGroup = $this->_getNextChildGroup($parentGroup, $group);
258 return $nextGroup;
259 }
260 else {
261 /* if we get here, it could be because we're out of siblings
262 * (in which case we return null) or because we're at the
263 * top level groups which do not have parents but may still
264 * have siblings, so check for that first.
265 */
266
267 $nextGroup = $this->_getNextParentlessGroup($group);
268 if ($nextGroup) {
269 $this->_lastParentlessGroup = $nextGroup;
270 return $nextGroup;
271 }
272 return NULL;
273 }
274 }
275
276 /**
277 * Adds a new child group identified by $childGroupId to the group
278 * identified by $groupId
279 *
280 * @param $parentID
281 * @param $childID
282 *
283 * @internal param \The $groupId id of the group to add the child to
284 * @internal param \The $childGroupId id of the new child group
285 *
286 * @return void
287 *
288 * @access public
289 */
290 static function add($parentID, $childID) {
291 // TODO: Add checks here to make sure invalid nests can't be created
292 $dao = new CRM_Contact_DAO_GroupNesting();
293 $query = "REPLACE INTO civicrm_group_nesting (child_group_id, parent_group_id) VALUES ($childID,$parentID);";
294 $dao->query($query);
295 }
296
297 /**
298 * Removes a child group identified by $childGroupId from the group
299 * identified by $groupId; does not delete child group, just the
300 * association between the two
301 *
302 * @param $parentID The id of the group to remove the child from
303 * @param $childID The id of the child group being removed
304 *
305 * @return void
306 *
307 * @access public
308 */
309 static function remove($parentID, $childID) {
310 $dao = new CRM_Contact_DAO_GroupNesting();
311 $query = "DELETE FROM civicrm_group_nesting WHERE child_group_id = $childID AND parent_group_id = $parentID";
312 $dao->query($query);
313 }
314
315 /**
316 * Removes associations where a child group is identified by $childGroupId from the group
317 * identified by $groupId; does not delete child group, just the
318 * association between the two
319 *
320 * @param $childID The id of the child group being removed
321 *
322 * @internal param \The $parentID id of the group to remove the child from
323 * @return void
324 *
325 * @access public
326 */
327 static function removeAllParentForChild($childID) {
328 $dao = new CRM_Contact_DAO_GroupNesting();
329 $query = "DELETE FROM civicrm_group_nesting WHERE child_group_id = $childID";
330 $dao->query($query);
331 }
332
333 /**
334 * Returns true if the association between parent and child is present,
335 * false otherwise.
336 *
337 * @param $parentID The parent id of the association
338 * @param $childID The child id of the association
339 *
340 * @return boolean True if association is found, false otherwise.
341 *
342 * @access public
343 */
344 static function isParentChild($parentID, $childID) {
345 $dao = new CRM_Contact_DAO_GroupNesting();
346 $query = "SELECT id FROM civicrm_group_nesting WHERE child_group_id = $childID AND parent_group_id = $parentID";
347 $dao->query($query);
348 if ($dao->fetch()) {
349 return TRUE;
350 }
351 return FALSE;
352 }
353
354 /**
355 * Returns true if if the given groupId has 1 or more child groups,
356 * false otherwise.
357 *
358 * @param $groupId The id of the group to check for child groups
359 *
360 * @return boolean True if 1 or more child groups are found, false otherwise.
361 *
362 * @access public
363 */
364 static function hasChildGroups($groupId) {
365 $dao = new CRM_Contact_DAO_GroupNesting();
366 $query = "SELECT child_group_id FROM civicrm_group_nesting WHERE parent_group_id = $groupId LIMIT 1";
367 //print $query . "\n<br><br>";
368 $dao->query($query);
369 if ($dao->fetch()) {
370 return TRUE;
371 }
372 return FALSE;
373 }
374
375 /**
376 * Returns true if the given groupId has 1 or more parent groups,
377 * false otherwise.
378 *
379 * @param $groupId The id of the group to check for parent groups
380 *
381 * @return boolean True if 1 or more parent groups are found, false otherwise.
382 *
383 * @access public
384 */
385 static function hasParentGroups($groupId) {
386 $dao = new CRM_Contact_DAO_GroupNesting();
387 $query = "SELECT parent_group_id FROM civicrm_group_nesting WHERE child_group_id = $groupId LIMIT 1";
388 $dao->query($query);
389 if ($dao->fetch()) {
390 return TRUE;
391 }
392 return FALSE;
393 }
394
395 /**
396 * Returns true if checkGroupId is a parent of one of the groups in
397 * groupIds, false otherwise.
398 *
399 * @param $groupIds Array of group ids (or one group id) to serve as the starting point
400 * @param $checkGroupId The group id to check if it is a parent of the $groupIds group(s)
401 *
402 * @return boolean True if $checkGroupId points to a group that is a parent of one of the $groupIds groups, false otherwise.
403 *
404 * @access public
405 */
406 static function isParentGroup($groupIds, $checkGroupId) {
407 if (!is_array($groupIds)) {
408 $groupIds = array($groupIds);
409 }
410 $dao = new CRM_Contact_DAO_GroupNesting();
411 $query = "SELECT parent_group_id FROM civicrm_group_nesting WHERE child_group_id IN (" . implode(',', $groupIds) . ")";
412 $dao->query($query);
413 while ($dao->fetch()) {
414 $parentGroupId = $dao->parent_group_id;
415 if ($parentGroupId == $checkGroupId) {
416 /* print "One of these: <pre>";
417 print_r($groupIds);
418 print "</pre> has groupId $checkGroupId as an ancestor.<br/>"; */
419
420 return TRUE;
421 }
422 }
423 return FALSE;
424 }
425
426 /**
427 * Returns true if checkGroupId is a child of one of the groups in
428 * groupIds, false otherwise.
429 *
430 * @param $groupIds Array of group ids (or one group id) to serve as the starting point
431 * @param $checkGroupId The group id to check if it is a child of the $groupIds group(s)
432 *
433 * @return boolean True if $checkGroupId points to a group that is a child of one of the $groupIds groups, false otherwise.
434 *
435 * @access public
436 */
437 static function isChildGroup($groupIds, $checkGroupId) {
438
439 if (!is_array($groupIds)) {
440 $groupIds = array($groupIds);
441 }
442 $dao = new CRM_Contact_DAO_GroupNesting();
443 $query = "SELECT child_group_id FROM civicrm_group_nesting WHERE parent_group_id IN (" . implode(',', $groupIds) . ")";
444 //print $query;
445 $dao->query($query);
446 while ($dao->fetch()) {
447 $childGroupId = $dao->child_group_id;
448 if ($childGroupId == $checkGroupId) {
449 /* print "One of these: <pre>";
450 print_r($groupIds);
451 print "</pre> has groupId $checkGroupId as a descendent.<br/><br/>"; */
452
453 return TRUE;
454 }
455 }
456 return FALSE;
457 }
458
459 /**
460 * Returns true if checkGroupId is an ancestor of one of the groups in
461 * groupIds, false otherwise.
462 *
463 * @param $groupIds Array of group ids (or one group id) to serve as the starting point
464 * @param $checkGroupId The group id to check if it is an ancestor of the $groupIds group(s)
465 *
466 * @return boolean True if $checkGroupId points to a group that is an ancestor of one of the $groupIds groups, false otherwise.
467 *
468 * @access public
469 */
470 static function isAncestorGroup($groupIds, $checkGroupId) {
471 if (!is_array($groupIds)) {
472 $groupIds = array($groupIds);
473 }
474 $dao = new CRM_Contact_DAO_GroupNesting();
475 $query = "SELECT parent_group_id FROM civicrm_group_nesting WHERE child_group_id IN (" . implode(',', $groupIds) . ")";
476 $dao->query($query);
477 $nextGroupIds = array();
478 $gotAtLeastOneResult = FALSE;
479 while ($dao->fetch()) {
480 $gotAtLeastOneResult = TRUE;
481 $parentGroupId = $dao->parent_group_id;
482 if ($parentGroupId == $checkGroupId) {
483 /* print "One of these: <pre>";
484 print_r($groupIds);
485 print "</pre> has groupId $checkGroupId as an ancestor.<br/>"; */
486
487 return TRUE;
488 }
489 $nextGroupIds[] = $parentGroupId;
490 }
491 if ($gotAtLeastOneResult) {
492 return self::isAncestorGroup($nextGroupIds, $checkGroupId);
493 }
494 else {
495 return FALSE;
496 }
497 }
498
499 /**
500 * Returns true if checkGroupId is a descendent of one of the groups in
501 * groupIds, false otherwise.
502 *
503 * @param $groupIds Array of group ids (or one group id) to serve as the starting point
504 * @param $checkGroupId The group id to check if it is a descendent of the $groupIds group(s)
505 *
506 * @return boolean True if $checkGroupId points to a group that is a descendent of one of the $groupIds groups, false otherwise.
507 *
508 * @access public
509 */
510 static function isDescendentGroup($groupIds, $checkGroupId) {
511 if (!is_array($groupIds)) {
512 $groupIds = array($groupIds);
513 }
514 $dao = new CRM_Contact_DAO_GroupNesting();
515 $query = "SELECT child_group_id FROM civicrm_group_nesting WHERE parent_group_id IN (" . implode(',', $groupIds) . ")";
516 $dao->query($query);
517 $nextGroupIds = array();
518 $gotAtLeastOneResult = FALSE;
519 while ($dao->fetch()) {
520 $gotAtLeastOneResult = TRUE;
521 $childGroupId = $dao->child_group_id;
522 if ($childGroupId == $checkGroupId) {
523 /* print "One of these: <pre>";
524 print_r($groupIds);
525 print "</pre> has groupId $checkGroupId as a descendent.<br/><br/>"; */
526
527 return TRUE;
528 }
529 $nextGroupIds[] = $childGroupId;
530 }
531 if ($gotAtLeastOneResult) {
532 return self::isDescendentGroup($nextGroupIds, $checkGroupId);
533 }
534 else {
535 return FALSE;
536 }
537 }
538
539 /**
540 * Returns array of group ids of ancestor groups of the specified group.
541 *
542 * @param $groupIds An array of valid group ids (passed by reference)
543 *
544 * @param bool $includeSelf
545 *
546 * @return array $groupIdArray List of groupIds that represent the requested group and its ancestors@access public
547 */
548 static function getAncestorGroupIds($groupIds, $includeSelf = TRUE) {
549 if (!is_array($groupIds)) {
550 $groupIds = array($groupIds);
551 }
552 $dao = new CRM_Contact_DAO_GroupNesting();
553 $query = "SELECT parent_group_id, child_group_id
554 FROM civicrm_group_nesting
555 WHERE child_group_id IN (" . implode(',', $groupIds) . ")";
556 $dao->query($query);
557 $tmpGroupIds = array();
558 $parentGroupIds = array();
559 if ($includeSelf) {
560 $parentGroupIds = $groupIds;
561 }
562 while ($dao->fetch()) {
563 // make sure we're not following any cyclical references
564 if (!array_key_exists($dao->child_group_id, $parentGroupIds) && $dao->parent_group_id != $groupIds[0]) {
565 $tmpGroupIds[] = $dao->parent_group_id;
566 }
567 }
568 if (!empty($tmpGroupIds)) {
569 $newParentGroupIds = self::getAncestorGroupIds($tmpGroupIds);
570 $parentGroupIds = array_merge($parentGroupIds, $newParentGroupIds);
571 }
572 return $parentGroupIds;
573 }
574
575 /**
576 * Returns array of ancestor groups of the specified group.
577 *
578 * @param $groupIds An array of valid group ids (passed by reference)
579 *
580 * @param bool $includeSelf
581 * @return \An $groupArray List of ancestor groups@access public
582 */
583 static function getAncestorGroups($groupIds, $includeSelf = TRUE) {
584 $groupIds = self::getAncestorGroupIds($groupIds, $includeSelf);
585 $params['id'] = $groupIds;
586 return CRM_Contact_BAO_Group::getGroups($params);
587 }
588
589 /**
590 * Returns array of group ids of child groups of the specified group.
591 *
592 * @param $groupIds An array of valid group ids (passed by reference)
593 *
594 * @return array $groupIdArray List of groupIds that represent the requested group and its children@access public
595 */
596 static function getChildGroupIds($groupIds) {
597 if (!is_array($groupIds)) {
598 $groupIds = array($groupIds);
599 }
600 $dao = new CRM_Contact_DAO_GroupNesting();
601 $query = "SELECT child_group_id FROM civicrm_group_nesting WHERE parent_group_id IN (" . implode(',', $groupIds) . ")";
602 $dao->query($query);
603 $childGroupIds = array();
604 while ($dao->fetch()) {
605 $childGroupIds[] = $dao->child_group_id;
606 }
607 return $childGroupIds;
608 }
609
610 /**
611 * Returns array of group ids of parent groups of the specified group.
612 *
613 * @param $groupIds An array of valid group ids (passed by reference)
614 *
615 * @return array $groupIdArray List of groupIds that represent the requested group and its parents@access public
616 */
617 static function getParentGroupIds($groupIds) {
618 if (!is_array($groupIds)) {
619 $groupIds = array($groupIds);
620 }
621 $dao = new CRM_Contact_DAO_GroupNesting();
622 $query = "SELECT parent_group_id FROM civicrm_group_nesting WHERE child_group_id IN (" . implode(',', $groupIds) . ")";
623 $dao->query($query);
624 $parentGroupIds = array();
625 while ($dao->fetch()) {
626 $parentGroupIds[] = $dao->parent_group_id;
627 }
628 return $parentGroupIds;
629 }
630
631 /**
632 * Returns array of group ids of descendent groups of the specified group.
633 *
634 * @param $groupIds An array of valid group ids (passed by reference)
635 *
636 * @param bool $includeSelf
637 * @return array $groupIdArray List of groupIds that represent the requested group and its descendents@access public
638 */
639 static function getDescendentGroupIds($groupIds, $includeSelf = TRUE) {
640 if (!is_array($groupIds)) {
641 $groupIds = array($groupIds);
642 }
643 $dao = new CRM_Contact_DAO_GroupNesting();
644 $query = "SELECT child_group_id, parent_group_id FROM civicrm_group_nesting WHERE parent_group_id IN (" . implode(',', $groupIds) . ")";
645 $dao->query($query);
646 $tmpGroupIds = array();
647 $childGroupIds = array();
648 if ($includeSelf) {
649 $childGroupIds = $groupIds;
650 }
651 while ($dao->fetch()) {
652 // make sure we're not following any cyclical references
653 if (!array_key_exists($dao->parent_group_id, $childGroupIds) && $dao->child_group_id != $groupIds[0]) {
654 $tmpGroupIds[] = $dao->child_group_id;
655 }
656 }
657 if (!empty($tmpGroupIds)) {
658 $newChildGroupIds = self::getDescendentGroupIds($tmpGroupIds);
659 $childGroupIds = array_merge($childGroupIds, $newChildGroupIds);
660 }
661 return $childGroupIds;
662 }
663
664 /**
665 * Returns array of descendent groups of the specified group.
666 *
667 * @param $groupIds An array of valid group ids (passed by reference)
668 *
669 * @param bool $includeSelf
670 * @return \An $groupArray List of descendent groups@access public
671 */
672 static function getDescendentGroups($groupIds, $includeSelf = TRUE) {
673 $groupIds = self::getDescendentGroupIds($groupIds, $includeSelf);
674 $params['id'] = $groupIds;
675 return CRM_Contact_BAO_Group::getGroups($params);
676 }
677
678 /**
679 * Returns array of group ids of valid potential child groups of the specified group.
680 *
681 * @param $groupId The group id to get valid potential children for
682 *
683 * @return array $groupIdArray List of groupIds that represent the valid potential children of the group@access public
684 */
685 static function getPotentialChildGroupIds($groupId) {
686 $groups = CRM_Contact_BAO_Group::getGroups();
687 $potentialChildGroupIds = array();
688 foreach ($groups as $group) {
689 $potentialChildGroupId = $group->id;
690 // print "Checking if $potentialChildGroupId is a descendent/ancestor of $groupId<br/><br/>";
691 if (!self::isDescendentGroup($groupId, $potentialChildGroupId) &&
692 !self::isAncestorGroup($groupId, $potentialChildGroupId) &&
693 $potentialChildGroupId != $groupId
694 ) {
695 $potentialChildGroupIds[] = $potentialChildGroupId;
696 }
697 }
698 return $potentialChildGroupIds;
699 }
700
701 /**
702 * @param $contactId
703 * @param $parentGroupId
704 *
705 * @return array
706 */
707 static function getContainingGroups($contactId, $parentGroupId) {
708 $groups = CRM_Contact_BAO_Group::getGroups();
709 $containingGroups = array();
710 foreach ($groups as $group) {
711 if (self::isDescendentGroup($parentGroupId, $group->id)) {
712 $members = CRM_Contact_BAO_Group::getMember($group->id);
713 if ($members[$contactId]) {
714 $containingGroups[] = $group->title;
715 }
716 }
717 }
718
719 return $containingGroups;
720 }
721 }
722