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