PageRenderTime 8ms CodeModel.GetById 4ms app.highlight 60ms RepoModel.GetById 2ms app.codeStats 0ms

/app/protected/modules/zurmo/utils/ReadPermissionsOptimizationUtil.php

https://bitbucket.org/markwu/zurmo
PHP | 1041 lines | 864 code | 74 blank | 103 comment | 80 complexity | 673c98740291960e7ec8bb5a506e36d9 MD5 | raw file
   1<?php
   2    /*********************************************************************************
   3     * Zurmo is a customer relationship management program developed by
   4     * Zurmo, Inc. Copyright (C) 2013 Zurmo Inc.
   5     *
   6     * Zurmo is free software; you can redistribute it and/or modify it under
   7     * the terms of the GNU Affero General Public License version 3 as published by the
   8     * Free Software Foundation with the addition of the following permission added
   9     * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
  10     * IN WHICH THE COPYRIGHT IS OWNED BY ZURMO, ZURMO DISCLAIMS THE WARRANTY
  11     * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
  12     *
  13     * Zurmo is distributed in the hope that it will be useful, but WITHOUT
  14     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  15     * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
  16     * details.
  17     *
  18     * You should have received a copy of the GNU Affero General Public License along with
  19     * this program; if not, see http://www.gnu.org/licenses or write to the Free
  20     * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  21     * 02110-1301 USA.
  22     *
  23     * You can contact Zurmo, Inc. with a mailing address at 27 North Wacker Drive
  24     * Suite 370 Chicago, IL 60606. or at email address contact@zurmo.com.
  25     *
  26     * The interactive user interfaces in original and modified versions
  27     * of this program must display Appropriate Legal Notices, as required under
  28     * Section 5 of the GNU Affero General Public License version 3.
  29     *
  30     * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
  31     * these Appropriate Legal Notices must retain the display of the Zurmo
  32     * logo and Zurmo copyright notice. If the display of the logo is not reasonably
  33     * feasible for technical reasons, the Appropriate Legal Notices must display the words
  34     * "Copyright Zurmo Inc. 2013. All rights reserved".
  35     ********************************************************************************/
  36
  37    abstract class ReadPermissionsOptimizationUtil
  38    {
  39        /**
  40         * At some point if performance is a problem with rebuilding activity models, then the stored procedure
  41         * needs to be refactored to somehow support more joins dynamically.
  42         * @see https://www.pivotaltracker.com/story/show/38804909
  43         * @param boolean $forcePhp
  44         */
  45        public static function rebuild($forcePhp = false)
  46        {
  47            //Forcing php way until we can fix failing tests here: AccountReadPermissionsOptimizationScenariosTest
  48            $forcePhp = true;
  49            assert('is_bool($forcePhp)');
  50            foreach (self::getMungableModelClassNames() as $modelClassName)
  51            {
  52                if (!SECURITY_OPTIMIZED || $forcePhp)
  53                {
  54                    self::rebuildViaSlowWay($modelClassName);
  55                }
  56                else
  57                {
  58                    //models that extend activity are special and can only be done with the PHP process.  They cannot
  59                    //be done using the stored procedure because it does not support the extra joins needed to determine
  60                    //which securable items to look at.
  61                    if (is_subclass_of($modelClassName, 'Activity'))
  62                    {
  63                        self::rebuildViaSlowWay($modelClassName);
  64                    }
  65                    else
  66                    {
  67                        $modelTableName = RedBeanModel::getTableName($modelClassName);
  68                        $mungeTableName = self::getMungeTableName($modelClassName);
  69                        if (!is_subclass_of($modelClassName, 'OwnedSecurableItem'))
  70                        {
  71                            throw new NotImplementedException($message, $code, $previous);
  72                        }
  73                        if (is_subclass_of($modelClassName, 'Person'))
  74                        {
  75                            if ($modelClassName != 'Contact')
  76                            {
  77                                throw new NotSupportedException();
  78                            }
  79                            else
  80                            {
  81                                $modelTableName = Person::getTableName('Person');
  82                            }
  83                        }
  84                        ZurmoDatabaseCompatibilityUtil::
  85                            callProcedureWithoutOuts("rebuild('$modelTableName', '$mungeTableName')");
  86                    }
  87                }
  88            }
  89        }
  90
  91        protected static function rebuildViaSlowWay($modelClassName)
  92        {
  93            // The slow way will remain here as documentation
  94            // for what the optimized way is doing.
  95            $mungeTableName  = self::getMungeTableName($modelClassName);
  96            self::recreateTable($mungeTableName);
  97            //Specifically call RedBeanModel to avoid the use of the security in OwnedSecurableItem since for
  98            //rebuild it needs to look at all models regardless of permissions of the current user.
  99            $modelCount = RedBeanModel::getCount(null, null, $modelClassName);
 100            $subset = intval($modelCount / 20);
 101            if ($subset < 100)
 102            {
 103                $subset = 100;
 104            }
 105            elseif ($subset > 1000)
 106            {
 107                $subset = 1000;
 108            }
 109            $users  = User::getAll();
 110            $groups = Group::getAll();
 111            $roles  = Role::getAll();
 112            for ($i = 0; $i < $modelCount; $i += $subset)
 113            {
 114                //Specifically call RedBeanModel to avoid the use of the security in OwnedSecurableItem since for
 115                //rebuild it needs to look at all models regardless of permissions of the current user.
 116                $models = RedBeanModel::getSubset(null, $i, $subset, null, null, $modelClassName);
 117                foreach ($models as $model)
 118                {
 119                    assert('$model instanceof SecurableItem');
 120                    $securableItemId = $model->getClassId('SecurableItem');
 121                    foreach ($users as $user)
 122                    {
 123                        list($allowPermissions, $denyPermissions) = $model->getExplicitActualPermissions($user);
 124                        $effectiveExplicitPermissions = $allowPermissions & ~$denyPermissions;
 125                        if (($effectiveExplicitPermissions & Permission::READ) == Permission::READ)
 126                        {
 127                            self::incrementCount($mungeTableName, $securableItemId, $user);
 128                        }
 129                    }
 130
 131                    foreach ($groups as $group)
 132                    {
 133                        list($allowPermissions, $denyPermissions) = $model->getExplicitActualPermissions($group);
 134                        $effectiveExplicitPermissions = $allowPermissions & ~$denyPermissions;
 135                        if (($effectiveExplicitPermissions & Permission::READ) == Permission::READ)
 136                        {
 137                            self::incrementCount($mungeTableName, $securableItemId, $group);
 138                            foreach ($group->users as $user)
 139                            {
 140                                if ($user->role->id > 0)
 141                                {
 142                                    self::incrementParentRolesCounts($mungeTableName, $securableItemId, $user->role);
 143                                }
 144                            }
 145                            foreach ($group->groups as $subGroup)
 146                            {
 147                                self::processNestedGroupWhereParentHasReadPermissionOnSecurableItem(
 148                                      $mungeTableName, $securableItemId, $subGroup);
 149                            }
 150                        }
 151                    }
 152                    foreach ($roles as $role)
 153                    {
 154                        $count = self::getRoleMungeCount($model, $role);
 155                        assert('$count >= 0');
 156                        if ($count > 0)
 157                        {
 158                            self::setCount($mungeTableName, $securableItemId, $role, $count);
 159                        }
 160                    }
 161                }
 162            }
 163        }
 164
 165        protected static function processNestedGroupWhereParentHasReadPermissionOnSecurableItem(
 166                                  $mungeTableName, $securableItemId, Group $group)
 167        {
 168            assert('is_string($mungeTableName) && $mungeTableName != ""');
 169            assert('is_int($securableItemId) && $securableItemId > 0');
 170            self::incrementCount($mungeTableName, $securableItemId, $group);
 171            foreach ($group->users as $user)
 172            {
 173                if ($user->role->id > 0)
 174                {
 175                    self::incrementParentRolesCounts($mungeTableName, $securableItemId, $user->role);
 176                }
 177            }
 178            foreach ($group->groups as $subGroup)
 179            {
 180                self::processNestedGroupWhereParentHasReadPermissionOnSecurableItem(
 181                      $mungeTableName, $securableItemId, $subGroup);
 182            }
 183        }
 184
 185        protected static function getRoleMungeCount(SecurableItem $securableItem, Role $role)
 186        {
 187            $count = 0;
 188            foreach ($role->roles as $subRole)
 189            {
 190                $count += self::getSubRoleMungeCount($securableItem, $subRole);
 191            }
 192            return $count;
 193        }
 194
 195        protected static function getSubRoleMungeCount(SecurableItem $securableItem, Role $role)
 196        {
 197            $count = self::getImmediateRoleMungeCount($securableItem, $role);
 198            foreach ($role->roles as $subRole)
 199            {
 200                $count += self::getSubRoleMungeCount($securableItem, $subRole);
 201            }
 202            return $count;
 203        }
 204
 205        protected static function getImmediateRoleMungeCount(SecurableItem $securableItem, Role $role)
 206        {
 207            $count = 0;
 208            foreach ($role->users as $user)
 209            {
 210                if ($securableItem->owner->isSame($user))
 211                {
 212                    $count++;
 213                }
 214                list($allowPermissions, $denyPermissions) = $securableItem->getExplicitActualPermissions($user);
 215                $effectiveExplicitPermissions = $allowPermissions & ~$denyPermissions;
 216                if (($effectiveExplicitPermissions & Permission::READ) == Permission::READ)
 217                {
 218                    $count++;
 219                }
 220                foreach ($user->groups as $group)
 221                {
 222                    $count += self::getGroupMungeCount($securableItem, $group);
 223                }
 224            }
 225            return $count;
 226        }
 227
 228        protected static function getGroupMungeCount(SecurableItem $securableItem, Group $group)
 229        {
 230            $count = 0;
 231            list($allowPermissions, $denyPermissions) = $securableItem->getExplicitActualPermissions($group);
 232            $effectiveExplicitPermissions = $allowPermissions & ~$denyPermissions;
 233            if (($effectiveExplicitPermissions & Permission::READ) == Permission::READ)
 234            {
 235                $count++;
 236            }
 237            if ($group->group->id > 0 && !(!RedBeanDatabase::isFrozen() && $group->group->isSame($group))) // Prevent cycles in database auto build.
 238            {
 239                $count += self::getGroupMungeCount($securableItem, $group->group);
 240            }
 241            return $count;
 242        }
 243
 244        // SecurableItem create, assigned, or deleted.
 245
 246        // Past tense implies the method must be called immediately after the associated operation.
 247        public static function ownedSecurableItemCreated(OwnedSecurableItem $ownedSecurableItem)
 248        {
 249            self::ownedSecurableItemOwnerChanged($ownedSecurableItem);
 250        }
 251
 252        public static function ownedSecurableItemOwnerChanged(OwnedSecurableItem $ownedSecurableItem, User $oldUser = null)
 253        {
 254            $modelClassName = get_class($ownedSecurableItem);
 255            assert('$modelClassName != "OwnedSecurableItem"');
 256            $mungeTableName = self::getMungeTableName($modelClassName);
 257            if ($oldUser !== null && $oldUser->role->id > 0)
 258            {
 259                self::decrementParentRolesCounts($mungeTableName, $ownedSecurableItem->getClassId('SecurableItem'), $oldUser->role);
 260                self::garbageCollect($mungeTableName);
 261            }
 262            if ($ownedSecurableItem->owner->role->id > 0)
 263            {
 264                self::incrementParentRolesCounts($mungeTableName, $ownedSecurableItem->getClassId('SecurableItem'), $ownedSecurableItem->owner->role);
 265            }
 266        }
 267
 268        // Being implies the the method must be called just before the associated operation.
 269        // The object is needed before the delete occurs and the delete cannot fail.
 270        public static function securableItemBeingDeleted(SecurableItem $securableItem) // Call being methods before the destructive operation.
 271        {
 272            $modelClassName = get_class($securableItem);
 273            assert('$modelClassName != "OwnedSecurableItem"');
 274            $mungeTableName = self::getMungeTableName($modelClassName);
 275            $securableItemId = $securableItem->getClassId('SecurableItem');
 276            R::exec("delete from $mungeTableName
 277                     where       securableitem_id = $securableItemId");
 278        }
 279
 280        // Permissions added or removed.
 281
 282        public static function securableItemGivenPermissionsForUser(SecurableItem $securableItem, User $user)
 283        {
 284            $modelClassName = get_class($securableItem);
 285            assert('$modelClassName != "OwnedSecurableItem"');
 286            $mungeTableName = self::getMungeTableName($modelClassName);
 287            $securableItemId = $securableItem->getClassId('SecurableItem');
 288            self::incrementCount($mungeTableName, $securableItemId, $user);
 289            if ($user->role->id > 0)
 290            {
 291                self::incrementParentRolesCounts($mungeTableName, $securableItemId, $user->role);
 292            }
 293        }
 294
 295        public static function securableItemGivenPermissionsForGroup(SecurableItem $securableItem, Group $group)
 296        {
 297            $modelClassName = get_class($securableItem);
 298            assert('$modelClassName != "OwnedSecurableItem"');
 299            $mungeTableName = self::getMungeTableName($modelClassName);
 300            $securableItemId = $securableItem->getClassId('SecurableItem');
 301            self::incrementCount($mungeTableName, $securableItemId, $group);
 302            foreach ($group->users as $user)
 303            {
 304                if ($user->role->id > 0)
 305                {
 306                    self::incrementParentRolesCounts($mungeTableName, $securableItemId, $user->role);
 307                }
 308            }
 309            foreach ($group->groups as $subGroup)
 310            {
 311                self::securableItemGivenPermissionsForGroup($securableItem, $subGroup);
 312            }
 313        }
 314
 315        public static function securableItemLostPermissionsForUser(SecurableItem $securableItem, User $user)
 316        {
 317            $modelClassName = get_class($securableItem);
 318            assert('$modelClassName != "OwnedSecurableItem"');
 319            $mungeTableName = self::getMungeTableName($modelClassName);
 320            $securableItemId = $securableItem->getClassId('SecurableItem');
 321            self::decrementCount($mungeTableName, $securableItemId, $user);
 322            if ($user->role->id > 0)
 323            {
 324                self::decrementParentRolesCounts($mungeTableName, $securableItemId, $user->role);
 325            }
 326            self::garbageCollect($mungeTableName);
 327        }
 328
 329        public static function securableItemLostPermissionsForGroup(SecurableItem $securableItem, Group $group)
 330        {
 331            $modelClassName = get_class($securableItem);
 332            assert('$modelClassName != "OwnedSecurableItem"');
 333            $mungeTableName = self::getMungeTableName($modelClassName);
 334            $securableItemId = $securableItem->getClassId('SecurableItem');
 335            self::decrementCount($mungeTableName, $securableItemId, $group);
 336            foreach ($group->users as $user)
 337            {
 338                self::securableItemLostPermissionsForUser($securableItem, $user);
 339            }
 340            foreach ($group->groups as $subGroup)
 341            {
 342                self::securableItemLostPermissionsForGroup($securableItem, $subGroup);
 343            }
 344            self::garbageCollect($mungeTableName);
 345        }
 346
 347        // User operations.
 348
 349        public static function userBeingDeleted($user) // Call being methods before the destructive operation.
 350        {
 351            foreach (self::getMungableModelClassNames() as $modelClassName)
 352            {
 353                $mungeTableName = self::getMungeTableName($modelClassName);
 354                if ($user->role->id > 0)
 355                {
 356                    self::decrementParentRolesCountsForAllSecurableItems($mungeTableName, $user->role);
 357                    self::garbageCollect($mungeTableName);
 358                }
 359                $userId = $user->id;
 360                R::exec("delete from $mungeTableName
 361                         where       munge_id = 'U$userId'");
 362            }
 363        }
 364
 365        // Group operations.
 366
 367        public static function userAddedToGroup(Group $group, User $user)
 368        {
 369            foreach (self::getMungableModelClassNames() as $modelClassName)
 370            {
 371                $mungeTableName = self::getMungeTableName($modelClassName);
 372                $groupId = $group->id;
 373                $sql = "select securableitem_id
 374                        from   $mungeTableName
 375                        where  munge_id = concat('G', $groupId)";
 376                $securableItemIds = R::getCol($sql);
 377                self::bulkIncrementParentRolesCounts($mungeTableName, $securableItemIds, $user->role);
 378                /*
 379                 * This extra step is not needed. See slide 21.  This is similar to userBeingRemovedFromRole in that
 380                 * the above query already is trapping the information needed.
 381                    Follow the same process for any upstream groups that the group is a member of.
 382                */
 383            }
 384        }
 385
 386        public static function userRemovedFromGroup(Group $group, User $user)
 387        {
 388            foreach (self::getMungableModelClassNames() as $modelClassName)
 389            {
 390                $mungeTableName = self::getMungeTableName($modelClassName);
 391                $groupId = $group->id;
 392                $sql = "select securableitem_id
 393                        from   $mungeTableName
 394                        where  munge_id = concat('G', $groupId)";
 395                $securableItemIds = R::getCol($sql);
 396                self::bulkDecrementParentRolesCounts($mungeTableName, $securableItemIds, $user->role);
 397                /*
 398                 * This extra step is not needed. See slide 22. This is similar to userBeingRemovedFromRole or
 399                 * userAddedToGroup in that the above query is already trapping the information needed.
 400                    Follow the same process for any upstream groups that the group is a member of.
 401                */
 402                self::garbageCollect($mungeTableName);
 403            }
 404        }
 405
 406        public static function groupAddedToGroup(Group $group)
 407        {
 408            self::groupAddedOrRemovedFromGroup(true, $group);
 409        }
 410
 411        public static function groupBeingRemovedFromGroup(Group $group) // Call being methods before the destructive operation.
 412        {
 413            self::groupAddedOrRemovedFromGroup(false, $group);
 414        }
 415
 416        public static function groupBeingDeleted($group) // Call being methods before the destructive operation.
 417        {
 418            if ($group->group->id > 0 && !(!RedBeanDatabase::isFrozen() && $group->group->isSame($group))) // Prevent cycles in database auto build.
 419            {
 420                self::groupBeingRemovedFromGroup($group);
 421            }
 422            foreach ($group->groups as $childGroup)
 423            {
 424                if (!RedBeanDatabase::isFrozen() && $group->isSame($childGroup)) // Prevent cycles in database auto build.
 425                {
 426                    continue;
 427                }
 428                self::groupBeingRemovedFromGroup($childGroup);
 429            }
 430            foreach ($group->users as $user)
 431            {
 432                self::userRemovedFromGroup($group, $user);
 433            }
 434            foreach (self::getMungableModelClassNames() as $modelClassName)
 435            {
 436                $groupId = $group->id;
 437                $mungeTableName = self::getMungeTableName($modelClassName);
 438                R::exec("delete from $mungeTableName
 439                     where       munge_id = 'G$groupId'");
 440            }
 441        }
 442
 443        protected static function groupAddedOrRemovedFromGroup($isAdd, Group $group)
 444        {
 445            assert('is_bool($isAdd)');
 446            if (!RedBeanDatabase::isFrozen() && $group->group->isSame($group)) // Prevent cycles in database auto build.
 447            {
 448                return;
 449            }
 450
 451            $countMethod1 = $isAdd ? 'bulkIncrementCount'             : 'bulkDecrementCount';
 452            $countMethod2 = $isAdd ? 'bulkIncrementParentRolesCounts' : 'bulkDecrementParentRolesCounts';
 453
 454            $parentGroups = self::getAllParentGroups($group);
 455            $users  = self::getAllUsersInGroupAndChildGroupsRecursively($group);
 456
 457            // Handle groups that $parentGroup is in. In/decrement for the containing groups' containing
 458            // groups the models they have explicit permissions on.
 459            // And handle user's role's parents. In/decrement for all users that have permission because
 460            // they are now in the containing group.
 461            if (count($parentGroups) > 0)
 462            {
 463                $parentGroupPermitableIds = array();
 464                foreach ($parentGroups as $parentGroup)
 465                {
 466                    $parentGroupPermitableIds[] = $parentGroup->getClassId('Permitable');
 467                }
 468                $sql = 'select securableitem_id
 469                        from   permission
 470                        where  permitable_id in (' . join(', ', $parentGroupPermitableIds) . ')';
 471                $securableItemIds = R::getCol($sql);
 472                foreach (self::getMungableModelClassNames() as $modelClassName)
 473                {
 474                    $mungeTableName = self::getMungeTableName($modelClassName);
 475                    self::$countMethod1($mungeTableName, $securableItemIds, $group);
 476                    foreach ($users as $user)
 477                    {
 478                        if ($user->role->id > 0)
 479                        {
 480                            self::$countMethod2($mungeTableName, $securableItemIds, $user->role);
 481                        }
 482                    }
 483                }
 484            }
 485            if (!$isAdd)
 486            {
 487                foreach (self::getMungableModelClassNames() as $modelClassName)
 488                {
 489                    $mungeTableName = self::getMungeTableName($modelClassName);
 490                    self::garbageCollect($mungeTableName);
 491                }
 492            }
 493        }
 494
 495        protected static function getAllUsersInGroupAndChildGroupsRecursively(Group $group)
 496        {
 497            $users = array();
 498            foreach ($group->users as $user)
 499            {
 500                $users[] = $user;
 501            }
 502            foreach ($group->groups as $childGroup)
 503            {
 504                if (!RedBeanDatabase::isFrozen() && $group->isSame($childGroup)) // Prevent cycles in database auto build.
 505                {
 506                    continue;
 507                }
 508                $users = array_merge($users, self::getAllUsersInGroupAndChildGroupsRecursively($childGroup));
 509            }
 510            return $users;
 511        }
 512
 513        protected static function getAllParentGroups(Group $group)
 514        {
 515            $parentGroups = array();
 516            $parentGroup = $group->group;
 517            while ($parentGroup->id > 0 && !(!RedBeanDatabase::isFrozen() && $parentGroup->isSame($parentGroup->group))) // Prevent cycles in database auto build.
 518            {
 519                $parentGroups[] = $parentGroup;
 520                $parentGroup = $parentGroup->group;
 521            }
 522            return $parentGroups;
 523        }
 524
 525        // Role operations.
 526
 527        public static function roleParentSet(Role $role)
 528        {
 529            assert('$role->role->id > 0');
 530            self::roleParentSetOrRemoved(true, $role);
 531        }
 532
 533        public static function roleParentBeingRemoved(Role $role) // Call being methods before the destructive operation.
 534        {
 535            assert('$role->role->id > 0');
 536            self::roleParentSetOrRemoved(false, $role);
 537        }
 538
 539        public static function roleBeingDeleted(Role $role) // Call being methods before the destructive operation.
 540        {
 541            foreach (self::getMungableModelClassNames() as $modelClassName)
 542            {
 543                if ($role->role->id > 0)
 544                {
 545                    self::roleParentBeingRemoved($role);
 546                }
 547                foreach ($role->roles as $childRole)
 548                {
 549                    if ($childRole->role->id > 0)
 550                    {
 551                        self::roleParentBeingRemoved($childRole);
 552                    }
 553                }
 554                $mungeTableName = self::getMungeTableName($modelClassName);
 555                $roleId = $role->id;
 556                $sql = "delete from $mungeTableName
 557                        where       munge_id = 'R$roleId'";
 558                R::exec($sql);
 559            }
 560        }
 561
 562        protected static function roleParentSetOrRemoved($isSet, Role $role)
 563        {
 564            assert('is_bool($isSet)');
 565            if (!RedBeanDatabase::isFrozen() && $role->role->isSame($role)) // Prevent cycles in database auto build.
 566            {
 567                return;
 568            }
 569
 570            $countMethod = $isSet ? 'bulkIncrementParentRolesCounts' : 'bulkDecrementParentRolesCounts';
 571
 572            foreach (self::getMungableModelClassNames() as $modelClassName)
 573            {
 574                $mungeTableName = self::getMungeTableName($modelClassName);
 575
 576                $usersInRolesChildren = self::getAllUsersInRolesChildRolesRecursively($role);
 577
 578                // Handle users in $role. In/decrement for the parent's parent
 579                // roles the models they either own or have explicit permissions on.
 580
 581                if (count($role->users) > 0)
 582                {
 583                    $userIds      = array();
 584                    $permitableIds = array();
 585                    foreach ($role->users as $user)
 586                    {
 587                        $userIds[]       = $user->id;
 588                        $permitableIds[] = $user->getClassId('Permitable');
 589                    }
 590                    $sql = 'select securableitem_id
 591                            from   ownedsecurableitem
 592                            where  owner__user_id in (' . join(', ', $userIds) . ')
 593                            union all
 594                            select securableitem_id
 595                            from   permission
 596                            where  permitable_id in (' . join(', ', $permitableIds) . ')';
 597                    $securableItemIds = R::getCol($sql);
 598                    self::$countMethod($mungeTableName, $securableItemIds, $role->role);
 599                }
 600
 601                // Handle users in the child roles of $role. Increment for the parent's parent
 602                // roles the models they either own or have explicit permissions on.
 603
 604                if (count($usersInRolesChildren))
 605                {
 606                    $userIds       = array();
 607                    $permitableIds = array();
 608                    foreach ($usersInRolesChildren as $user)
 609                    {
 610                        $userIds[]       = $user->id;
 611                        $permitableIds[] = $user->getClassId('Permitable');
 612                    }
 613                    $sql = 'select securableitem_id
 614                            from   ownedsecurableitem
 615                            where  owner__user_id in (' . join(', ', $userIds) . ')
 616                            union all
 617                            select securableitem_id
 618                            from   permission
 619                            where  permitable_id in (' . join(', ', $permitableIds) . ')';
 620                    $securableItemIds = R::getCol($sql);
 621                    self::$countMethod($mungeTableName, $securableItemIds, $role);
 622                }
 623
 624                // Handle groups for the users in $role. Increment for the parent's parent
 625                // roles the models they have explicit permissions on.
 626
 627                if (count($role->users) > 0)
 628                {
 629                    $permitableIds = array();
 630                    foreach ($role->users as $user)
 631                    {
 632                        foreach ($user->groups as $group)
 633                        {
 634                            $permitableIds[] = $group->getClassId('Permitable');
 635                        }
 636                    }
 637                    $permitableIds = array_unique($permitableIds);
 638                    $sql = 'select securableitem_id
 639                            from   permission
 640                            where  permitable_id in (' . join(', ', $permitableIds) . ')';
 641                    $securableItemIds = R::getCol($sql);
 642                    self::$countMethod($mungeTableName, $securableItemIds, $role->role);
 643                }
 644
 645                // Handle groups for the users $role's child roles. Increment for the role's parent
 646                // roles the models they have explicit permissions on.
 647
 648                if (count($usersInRolesChildren))
 649                {
 650                    $permitableIds = array();
 651                    foreach ($usersInRolesChildren as $user)
 652                    {
 653                        foreach ($user->groups as $group)
 654                        {
 655                            $permitableIds[] = $group->getClassId('Permitable');
 656                        }
 657                    }
 658                    $permitableIds = array_unique($permitableIds);
 659                    if (count($permitableIds) > 0)
 660                    {
 661                        $sql = 'select securableitem_id
 662                                from   permission
 663                                where  permitable_id in (' . join(', ', $permitableIds) . ')';
 664                        $securableItemIds = R::getCol($sql);
 665                    }
 666                    else
 667                    {
 668                        $securableItemIds = array();
 669                    }
 670                    self::$countMethod($mungeTableName, $securableItemIds, $role);
 671                }
 672                if (!$isSet)
 673                {
 674                    self::garbageCollect($mungeTableName);
 675                }
 676            }
 677        }
 678
 679        protected static function getAllUsersInRolesChildRolesRecursively(Role $role)
 680        {
 681            $users = array();
 682            foreach ($role->roles as $childRole)
 683            {
 684                if (!RedBeanDatabase::isFrozen() && $role->isSame($childRole)) // Prevent cycles in database auto build.
 685                {
 686                    continue;
 687                }
 688                foreach ($childRole->users as $user)
 689                {
 690                    $users[] = $user;
 691                }
 692                $users = array_merge($users, self::getAllUsersInRolesChildRolesRecursively($childRole));
 693            }
 694            return $users;
 695        }
 696
 697        public static function userAddedToRole(User $user)
 698        {
 699            assert('$user->role->id > 0');
 700            foreach (self::getMungableModelClassNames() as $modelClassName)
 701            {
 702                $mungeTableName = self::getMungeTableName($modelClassName);
 703                $userId = $user->id;
 704                $sql = "select securableitem_id
 705                        from   ownedsecurableitem
 706                        where  owner__user_id = $userId";
 707                $securableItemIds = R::getCol($sql);
 708                //Increment the parent roles for securableItems that the user is the owner on.
 709                self::bulkIncrementParentRolesCounts($mungeTableName, $securableItemIds, $user->role);
 710
 711                //Get all downstream groups the user is in including any groups that are in those groups recursively.
 712                //Then for each group found, add weight for the user's upstream roles.
 713                $groupMungeIds = array();
 714                foreach ($user->groups as $group)
 715                {
 716                    $groupMungeIds[] = 'G' . $group->id;
 717                    self::getAllUpstreamGroupsRecursively($group, $groupMungeIds);
 718                }
 719                if (count($groupMungeIds) > 0)
 720                {
 721                    $inSqlPart = SQLOperatorUtil::resolveOperatorAndValueForOneOf('oneOf', $groupMungeIds, true);
 722                    $sql = "select distinct $mungeTableName.securableitem_id
 723                            from   $mungeTableName
 724                            where  $mungeTableName.munge_id $inSqlPart";
 725                    $securableItemIds = R::getCol($sql);
 726                    self::bulkIncrementParentRolesCounts($mungeTableName, $securableItemIds, $user->role);
 727                }
 728            }
 729        }
 730
 731        public static function userBeingRemovedFromRole(User $user, Role $role)
 732        {
 733            foreach (self::getMungableModelClassNames() as $modelClassName)
 734            {
 735                $mungeTableName = self::getMungeTableName($modelClassName);
 736                $userId = $user->id;
 737                $sql = "select securableitem_id
 738                        from   ownedsecurableitem
 739                        where  owner__user_id = $userId";
 740                $securableItemIds = R::getCol($sql);
 741                self::bulkDecrementParentRolesCounts($mungeTableName, $securableItemIds, $role);
 742
 743                $sql = "select $mungeTableName.securableitem_id
 744                        from   $mungeTableName, _group__user
 745                        where  $mungeTableName.munge_id = concat('G', _group__user._group_id) and
 746                               _group__user._user_id = $userId";
 747                $securableItemIds = R::getCol($sql);
 748                self::bulkDecrementParentRolesCounts($mungeTableName, $securableItemIds, $role);
 749                /*
 750                 * This additional step I don't think is needed because the sql query above actually traps
 751                 * the upstream explicit securableItems because the lower level groups will already have a point for
 752                 * each of them.
 753                    What groups are the user part of and what groups are those groups children of recursively?
 754                    For any models that have that group explicity for read, subtract 1 point for the user's
 755                    upstream roles from the disconnected role.
 756                */
 757                self::garbageCollect($mungeTableName);
 758            }
 759        }
 760
 761        ///////////////////////////////////////////////////////////////////////
 762
 763        public static function getAllUpstreamGroupsRecursively(Group $group, & $groupMungeIds)
 764        {
 765            assert('is_array($groupMungeIds)');
 766            if ($group->group->id > 0 )
 767            {
 768                $groupMungeIds[] = 'G' . $group->group->id;
 769                if (!RedBeanDatabase::isFrozen() && $group->isSame($group->group))
 770                {
 771                    //Do Nothing. Prevent cycles in database auto build.
 772                }
 773                else
 774                {
 775                    self::getAllUpstreamGroupsRecursively($group->group, $groupMungeIds);
 776                }
 777            }
 778        }
 779
 780        public static function getUserRoleIdAndGroupIds(User $user)
 781        {
 782            if ($user->role->id > 0)
 783            {
 784                $roleId = $user->role->id;
 785            }
 786            else
 787            {
 788                $roleId = null;
 789            }
 790            $groupIds = array();
 791            foreach ($user->groups as $group)
 792            {
 793                $groupIds[] = $group->id;
 794            }
 795            return array($roleId, $groupIds);
 796        }
 797
 798        public static function getMungeIdsByUser(User $user)
 799        {
 800            list($roleId, $groupIds) = self::getUserRoleIdAndGroupIds($user);
 801            $mungeIds = array("U$user->id");
 802            if ($roleId != null)
 803            {
 804                $mungeIds[] = "R$roleId";
 805            }
 806            foreach ($groupIds as $groupId)
 807            {
 808                $mungeIds[] = "G$groupId";
 809            }
 810            //Add everyone group
 811            $everyoneGroupId = Group::getByName(Group::EVERYONE_GROUP_NAME)->id;
 812            if (!in_array("G" . $everyoneGroupId, $mungeIds) && $everyoneGroupId > 0)
 813            {
 814                $mungeIds[] = "G" . $everyoneGroupId;
 815            }
 816            return $mungeIds;
 817        }
 818
 819        /**
 820         * Public for testing only. Need to manually create test model tables that would not be picked up normally.
 821         */
 822        public static function recreateTable($mungeTableName)
 823        {
 824            assert('is_string($mungeTableName) && $mungeTableName  != ""');
 825            R::exec("drop table if exists $mungeTableName");
 826            R::exec("create table $mungeTableName (
 827                        securableitem_id int(11)     unsigned not null,
 828                        munge_id         varchar(12)          not null,
 829                        count            int(8)      unsigned not null,
 830                        primary key (securableitem_id, munge_id)
 831                     )");
 832            R::exec("create index index_${mungeTableName}_securable_item_id
 833                     on $mungeTableName (securableitem_id);");
 834        }
 835
 836        protected static function incrementCount($mungeTableName, $securableItemId, $item)
 837        {
 838            assert('is_string($mungeTableName) && $mungeTableName != ""');
 839            assert('is_int($securableItemId) && $securableItemId > 0');
 840            assert('$item instanceof User || $item instanceof Group || $item instanceof Role');
 841            $itemId  = $item->id;
 842            $type    = self::getMungeType($item);
 843            $mungeId = "$type$itemId";
 844            R::exec("insert into $mungeTableName
 845                                 (securableitem_id, munge_id, count)
 846                     values ($securableItemId, '$mungeId', 1)
 847                     on duplicate key
 848                     update count = count + 1");
 849        }
 850
 851        protected static function setCount($mungeTableName, $securableItemId, $item, $count)
 852        {
 853            assert('is_string($mungeTableName) && $mungeTableName != ""');
 854            assert('is_int($securableItemId) && $securableItemId > 0');
 855            assert('$item instanceof User || $item instanceof Group || $item instanceof Role');
 856            $itemId  = $item->id;
 857            $type    = self::getMungeType($item);
 858            $mungeId = "$type$itemId";
 859            R::exec("insert into $mungeTableName
 860                                 (securableitem_id, munge_id, count)
 861                     values ($securableItemId, '$mungeId', $count)
 862                     on duplicate key
 863                     update count = $count");
 864        }
 865
 866        protected static function decrementCount($mungeTableName, $securableItemId, $item)
 867        {
 868            assert('is_string($mungeTableName) && $mungeTableName != ""');
 869            assert('is_int($securableItemId) && $securableItemId > 0');
 870            assert('$item instanceof User || $item instanceof Group || $item instanceof Role');
 871            $itemId  = $item->id;
 872            $type    = self::getMungeType($item);
 873            $mungeId = "$type$itemId";
 874            R::exec("update $mungeTableName
 875                     set count = count - 1
 876                     where securableitem_id = $securableItemId and
 877                           munge_id         = '$mungeId'");
 878        }
 879
 880        protected static function decrementCountForAllSecurableItems($mungeTableName, $item)
 881        {
 882            assert('is_string($mungeTableName) && $mungeTableName != ""');
 883            assert('$item instanceof User || $item instanceof Group || $item instanceof Role');
 884            $itemId  = $item->id;
 885            $type    = self::getMungeType($item);
 886            $mungeId = "$type$itemId";
 887            R::exec("update $mungeTableName
 888                     set count = count - 1
 889                     where munge_id = '$mungeId'");
 890        }
 891
 892        protected static function bulkIncrementCount($mungeTableName, $securableItemIds, $item)
 893        {
 894            assert('is_string($mungeTableName) && $mungeTableName != ""');
 895            assert('$item instanceof User || $item instanceof Group || $item instanceof Role');
 896            foreach ($securableItemIds as $securableItemId)
 897            {
 898                self::incrementCount($mungeTableName, intval($securableItemId), $item);
 899            }
 900        }
 901
 902        protected static function bulkDecrementCount($mungeTableName, $securableItemIds, $item)
 903        {
 904            assert('is_string($mungeTableName) && $mungeTableName != ""');
 905            assert('$item instanceof User || $item instanceof Group || $item instanceof Role');
 906            foreach ($securableItemIds as $securableItemId)
 907            {
 908                self::decrementCount($mungeTableName, intval($securableItemId), $item);
 909            }
 910        }
 911
 912        protected static function incrementParentRolesCounts($mungeTableName, $securableItemId, Role $role)
 913        {
 914            assert('is_string($mungeTableName) && $mungeTableName != ""');
 915            assert('is_int($securableItemId) && $securableItemId > 0');
 916            if (!RedBeanDatabase::isFrozen() && $role->role->isSame($role)) // Prevent cycles in database auto build.
 917            {
 918                return;
 919            }
 920            if ($role->role->id > 0)
 921            {
 922                self::incrementCount            ($mungeTableName, $securableItemId, $role->role);
 923                self::incrementParentRolesCounts($mungeTableName, $securableItemId, $role->role);
 924            }
 925        }
 926
 927        protected static function decrementParentRolesCounts($mungeTableName, $securableItemId, Role $role)
 928        {
 929            assert('is_string($mungeTableName) && $mungeTableName != ""');
 930            assert('is_int($securableItemId) && $securableItemId > 0');
 931            if (!RedBeanDatabase::isFrozen() && $role->role->isSame($role)) // Prevent cycles in database auto build.
 932            {
 933                return;
 934            }
 935            if ($role->role->id > 0)
 936            {
 937                self::decrementCount            ($mungeTableName, $securableItemId, $role->role);
 938                self::decrementParentRolesCounts($mungeTableName, $securableItemId, $role->role);
 939            }
 940        }
 941
 942        protected static function decrementParentRolesCountsForAllSecurableItems($mungeTableName, Role $role)
 943        {
 944            assert('is_string($mungeTableName) && $mungeTableName != ""');
 945            if (!RedBeanDatabase::isFrozen() && $role->role->isSame($role)) // Prevent cycles in database auto build.
 946            {
 947                return;
 948            }
 949            if ($role->role->id > 0)
 950            {
 951                self::decrementCountForAllSecurableItems            ($mungeTableName, $role->role);
 952                self::decrementParentRolesCountsForAllSecurableItems($mungeTableName, $role->role);
 953            }
 954        }
 955
 956        protected static function bulkIncrementParentRolesCounts($mungeTableName, $securableItemIds, Role $role)
 957        {
 958            foreach ($securableItemIds as $securableItemId)
 959            {
 960                self::incrementParentRolesCounts($mungeTableName, intval($securableItemId), $role);
 961            }
 962        }
 963
 964        protected static function bulkDecrementParentRolesCounts($mungeTableName, $securableItemIds, Role $role)
 965        {
 966            foreach ($securableItemIds as $securableItemId)
 967            {
 968                self::decrementParentRolesCounts($mungeTableName, intval($securableItemId), $role);
 969            }
 970        }
 971
 972        // This must be called ny any public method which decrements
 973        // counts after it has done all its count decrementing.
 974        // It is not done in decrementCount to avoid doing it more
 975        // than is necessary.
 976        protected static function garbageCollect($mungeTableName)
 977        {
 978            assert("(int)R::getCell('select count(*)
 979                                from   $mungeTableName
 980                                where  count < 0') == 0");
 981            R::exec("delete from $mungeTableName
 982                     where       count = 0");
 983            assert("(int)R::getCell('select count(*)
 984                                from   $mungeTableName
 985                                where  count < 1') == 0");
 986        }
 987
 988        protected static function getMungeType($item)
 989        {
 990            assert('$item instanceof User || $item instanceof Group || $item instanceof Role');
 991            return substr(get_class($item), 0, 1);
 992        }
 993
 994        //public for testing only
 995        public static function getMungableModelClassNames()
 996        {
 997            try
 998            {
 999                return GeneralCache::getEntry('mungableModelClassNames');
1000            }
1001            catch (NotFoundException $e)
1002            {
1003                $mungableClassNames = self::findMungableModelClassNames();
1004                GeneralCache::cacheEntry('mungableModelClassNames', $mungableClassNames);
1005                return $mungableClassNames;
1006            }
1007        }
1008
1009        //public for testing only.
1010        public static function findMungableModelClassNames()
1011        {
1012            $mungableModelClassNames = array();
1013            $modules = Module::getModuleObjects();
1014            foreach ($modules as $module)
1015            {
1016                $modelClassNames = $module::getModelClassNames();
1017                foreach ($modelClassNames as $modelClassName)
1018                {
1019                    if (is_subclass_of($modelClassName, 'SecurableItem') &&
1020                        $modelClassName::hasReadPermissionsOptimization())
1021                    {
1022                        $mungableModelClassNames[] = $modelClassName;
1023                    }
1024                }
1025            }
1026            return $mungableModelClassNames;
1027        }
1028
1029        protected static function getMainTableName($modelClassName)
1030        {
1031            assert('is_string($modelClassName) && $modelClassName != ""');
1032            return RedBeanModel::getTableName($modelClassName);
1033        }
1034
1035        public static function getMungeTableName($modelClassName)
1036        {
1037            assert('is_string($modelClassName) && $modelClassName != ""');
1038            return self::getMainTableName($modelClassName) . '_read';
1039        }
1040    }
1041?>