1<?php 2 3namespace go\core\acl\model; 4 5use Exception; 6use go\core\exception\Forbidden; 7use go\core\model\Acl; 8use go\core\orm\Query; 9use go\core\db\Query as DbQuery; 10use go\core\jmap\EntityController; 11use go\core\jmap\Entity; 12 13/** 14 * The AclItemEntity class 15 * 16 * Is used for items that belong to an entity which is an {@see AclEntity}. 17 * For examples a Note is an AclItemEntity because it belongs to the NoteBook AclEntity. 18 * 19 * It's main purpose is to provide the {@see applyAclToQuery()} function so you 20 * can easily query items which a user has read permissions for. 21 * 22 * You can also specify another AclItemEntity so it will recurse. 23 * 24 * @see AclOwnerEntity 25 */ 26abstract class AclItemEntity extends AclEntity { 27 28 /** 29 * Fires when the ACL has changed. 30 * 31 * Not when changes were made to the acl but when the complete list has been replaced when for example 32 * a contact has been moved to another address book. * 33 */ 34 const EVENT_ACL_CHANGED = 'aclchanged'; 35 36 /** 37 * Get the {@see AclOwnerEntity} or {@see AclItemEntity} class name that it 38 * depends on. 39 * 40 * @return string 41 */ 42 abstract protected static function aclEntityClass(); 43 44 /** 45 * Get the keys for joining the aclEntityClass table. 46 * 47 * @return array eg. ['folderId' => 'id'] 48 */ 49 abstract protected static function aclEntityKeys(); 50 51 /** 52 * Applies conditions to the query so that only entities with the given permission level are fetched. 53 * 54 * @param Query $query 55 * @param int $level 56 * @param int $userId Defaults to current user ID 57 * @param int[] $groups Supply user groups to check. $userId must be null when usoing this. Leave to null for the current user 58 * @return Query 59 * @throws Exception 60 */ 61 public static function applyAclToQuery(Query $query, $level = Acl::LEVEL_READ, $userId = null, $groups = null) { 62 63 $alias = self::joinAclEntity($query); 64 65 Acl::applyToQuery($query, $alias, $level, $userId, $groups); 66 67 return $query; 68 } 69 70 /** 71 * Log's deleted entities for JMAP sync 72 * 73 * @param Query $query The query to select entities in the delete statement 74 * @return boolean 75 * @throws Exception 76 */ 77 protected static function logDeleteChanges(Query $query) { 78 79 $table = self::getMapping()->getPrimaryTable(); 80 $changes = clone $query; 81 $changes->select($table->getAlias() . '.id as entityId'); 82 83 $alias = static::joinAclEntity($changes); 84 85 $changes->select($alias . ', "1" as destroyed', true); 86 87 return static::entityType()->changes($changes); 88 } 89 90 /** 91 * Join's the ACL owner entity primary table 92 * 93 * @param DbQuery $query 94 * @param null $fromAlias 95 * @return string Alias for the acl column. For example: "addressbook.aclId" 96 * @throws Exception 97 */ 98 public static function joinAclEntity(DbQuery $query, $fromAlias = null) { 99 $cls = static::aclEntityClass(); 100 101 /* @var $cls Entity */ 102 103 if(!isset($fromAlias)) { 104 $fromAlias = $query->getTableAlias(); 105 } 106 107 $keys = []; 108 foreach (static::aclEntityKeys() as $from => $to) { 109 $column = $cls::getMapping()->getColumn($to); 110 111 $keys[] = $fromAlias . '.' . $from . ' = ' . $column->table->getAlias() . ' . '. $to; 112 } 113 114 // Override didn't work because on delete it did need to be joined. 115// if($query->isJoined($column->table->getName(), $column->table->getAlias())) { 116// throw new \Exception( 117// "The ACL owner table `". $column->table->getName() . 118// "` was already joined with alias `" . $column->table->getAlias() . 119// "` in class " . static::class . ". If you joined this table via defineMapping() then override the method joinAclEntity() and return '" . $column->table->getAlias() . '.' . $cls::$aclColumnName ."'.") ; 120// } 121 122 if(!$query->isJoined($column->table->getName(), $column->table->getAlias())) { 123 $query->join($column->table->getName(), $column->table->getAlias(), implode(' AND ', $keys)); 124 } 125 126 127 //If this is another AclItemEntity then recurse 128 if(is_a($cls, AclItemEntity::class, true)) { 129 return $cls::joinAclEntity($query, $column->table->getAlias()); 130 } else 131 { 132 //otherwise this must hold the aclId column 133 $aclColumn = $cls::getMapping()->getColumn($cls::$aclColumnName); 134 if(!$aclColumn) { 135 throw new Exception("Column 'aclId' is required for AclEntity '$cls'"); 136 } 137 138 return $column->table->getAlias() . '.' . $cls::$aclColumnName; 139 } 140 } 141 142 /** 143 * Get the table alias holding the aclId 144 */ 145 public static function getAclEntityTableAlias() { 146 147 $cls = static::aclEntityClass(); 148 149 /* @var $cls Entity */ 150 151 //If this is another AclItemEntity then recurse 152 if(is_a($cls, AclItemEntity::class, true)) { 153 return $cls::getAclEntityTableAlias(); 154 } else 155 { 156 //otherwise this must hold the aclId column 157 $aclColumn = $cls::getMapping()->getColumn('aclId'); 158 if(!$aclColumn) { 159 throw new Exception("Column 'aclId' is required for AclEntity '$cls'"); 160 } 161 162 return $aclColumn->table->getAlias(); 163 } 164 } 165 166 /** 167 * Get the entity that holds the acl id. 168 * 169 * @return Entity 170 */ 171 protected function getAclEntity() { 172 $cls = static::aclEntityClass(); 173 174 /* @var $cls Entity */ 175 176 177 $keys = []; 178 foreach (static::aclEntityKeys() as $from => $to) { 179 if(!isset($this->{$from})) { 180 throw new Exception("Required property '".static::class."::$from' not fetched"); 181 } 182 $keys[$to] = $this->{$from}; 183 } 184 185 $aclEntity = $cls::find($cls::getMapping()->getColumnNames())->where($keys)->single(); 186 187 if(!$aclEntity) { 188 throw new Exception("Can't find related ACL entity. The keys must be invalid: " . var_export($keys, true)); 189 } 190 191 return $aclEntity; 192 } 193 194 private function isAclChanged() 195 { 196 return $this->isModified(array_keys(static::aclEntityKeys())); 197 } 198 199 protected function internalSave() 200 { 201 if(!$this->isNew() && $this->isAclChanged()) { 202 static::fireEvent(self::EVENT_ACL_CHANGED, $this); 203 } 204 205 return parent::internalSave(); 206 } 207 208 /** 209 * @inheritDoc 210 */ 211 public function getPermissionLevel() { 212 213 if(!isset($this->permissionLevel)) { 214 $aclEntity = $this->getAclEntity(); 215 216 $this->permissionLevel = $aclEntity->getPermissionLevel(); 217 } 218 219 return $this->permissionLevel; 220 } 221 222 /** 223 * Finds all aclId's for this entity 224 * 225 * This query is used in the "getFooUpdates" methods of entities to determine if any of the ACL's has been changed. 226 * If so then the server will respond that it cannot calculate the updates. 227 * 228 * @see EntityController::getUpdates() 229 * 230 * @return Query 231 */ 232 public static function findAcls() { 233 234 $cls = static::aclEntityClass(); 235 236 return $cls::findAcls(); 237 } 238 239 /** 240 * Find the ACL id that holds the permissions for this item 241 * 242 * @return int 243 * @throws Exception 244 */ 245 public function findAclId() { 246 return $this->getAclEntity()->findAclId(); 247 } 248 249} 250