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