Apply PSR-12 code style (#3886)
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / trophy / condition / TrophyConditionHandler.class.php
1 <?php
2
3 namespace wcf\system\trophy\condition;
4
5 use wcf\data\object\type\ObjectType;
6 use wcf\data\object\type\ObjectTypeCache;
7 use wcf\data\trophy\Trophy;
8 use wcf\data\trophy\TrophyList;
9 use wcf\data\user\trophy\UserTrophyAction;
10 use wcf\data\user\UserList;
11 use wcf\system\SingletonFactory;
12
13 /**
14 * Handles trophy conditions.
15 *
16 * @author Joshua Ruesweg
17 * @copyright 2001-2019 WoltLab GmbH
18 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
19 * @package WoltLabSuite\Core\System\Trophy\Condition
20 * @since 3.1
21 */
22 class TrophyConditionHandler extends SingletonFactory
23 {
24 /**
25 * definition name for trophy conditions
26 * @var string
27 */
28 const CONDITION_DEFINITION_NAME = 'com.woltlab.wcf.condition.trophy';
29
30 /**
31 * list of grouped trophy condition object types
32 * @var ObjectType[][]
33 */
34 protected $groupedObjectTypes = [];
35
36 /**
37 * @inheritDoc
38 */
39 protected function init()
40 {
41 $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes(self::CONDITION_DEFINITION_NAME);
42
43 foreach ($objectTypes as $objectType) {
44 if (!$objectType->conditiongroup) {
45 continue;
46 }
47
48 if (!isset($this->groupedObjectTypes[$objectType->conditiongroup])) {
49 $this->groupedObjectTypes[$objectType->conditiongroup] = [];
50 }
51
52 $this->groupedObjectTypes[$objectType->conditiongroup][$objectType->objectTypeID] = $objectType;
53 }
54 }
55
56 /**
57 * Returns the list of grouped trophy condition object types.
58 *
59 * @return ObjectType[][]
60 */
61 public function getGroupedObjectTypes()
62 {
63 return $this->groupedObjectTypes;
64 }
65
66 /**
67 * Assign trophies based on rules.
68 *
69 * @param int $maxAssigns
70 */
71 public function assignTrophies($maxAssigns = 500)
72 {
73 $trophyList = new TrophyList();
74 $trophyList->getConditionBuilder()->add('awardAutomatically = ?', [1]);
75 $trophyList->getConditionBuilder()->add('isDisabled = ?', [0]);
76 $trophyList->readObjects();
77
78 $i = 0;
79 foreach ($trophyList as $trophy) {
80 $userIDs = $this->getUserIDs($trophy);
81
82 foreach ($userIDs as $userID) {
83 (new UserTrophyAction([], 'create', [
84 'data' => [
85 'trophyID' => $trophy->trophyID,
86 'userID' => $userID,
87 'time' => TIME_NOW,
88 ],
89 ]))->executeAction();
90
91 if (++$i >= $maxAssigns) {
92 return;
93 }
94 }
95 }
96 }
97
98 /**
99 * Revoke user trophies which are not longer fulfills the conditions.
100 *
101 * @param int $maxRevokes
102 * @since 5.2
103 */
104 public function revokeTrophies($maxRevokes = 500)
105 {
106 $trophyList = new TrophyList();
107 $trophyList->getConditionBuilder()->add('awardAutomatically = ?', [1]);
108 $trophyList->getConditionBuilder()->add('revokeAutomatically = ?', [1]);
109 $trophyList->getConditionBuilder()->add('isDisabled = ?', [0]);
110 $trophyList->readObjects();
111
112 $i = 0;
113 foreach ($trophyList as $trophy) {
114 $userTrophyIDs = $this->getRevocableUserTrophyIDs($trophy, $maxRevokes - $i);
115
116 $i += \count($userTrophyIDs);
117
118 (new UserTrophyAction($userTrophyIDs, 'delete'))->executeAction();
119
120 if ($i >= $maxRevokes) {
121 return;
122 }
123 }
124 }
125
126 /**
127 * Returns the users who fulfill all conditions of the given trophy.
128 *
129 * @param Trophy $trophy
130 * @return int[]
131 * @since 5.2
132 */
133 private function getUserIDs(Trophy $trophy)
134 {
135 $userList = new UserList();
136 $userList->sqlConditionJoins .= " LEFT JOIN wcf" . WCF_N . "_user_option_value user_option_value ON (user_option_value.userID = user_table.userID)";
137
138 $conditions = $trophy->getConditions();
139 foreach ($conditions as $condition) {
140 $condition->getObjectType()->getProcessor()->addUserCondition($condition, $userList);
141 }
142
143 // prevent multiple awards from a trophy for a user
144 $userList->getConditionBuilder()->add(
145 'user_table.userID NOT IN (SELECT userID FROM wcf' . WCF_N . '_user_trophy WHERE trophyID IN (?))',
146 [$trophy->trophyID]
147 );
148 $userList->readObjectIDs();
149
150 return $userList->getObjectIDs();
151 }
152
153 /**
154 * Returns the userTrophyIDs of the users, which no longer fulfills the trophy conditions.
155 *
156 * @param Trophy $trophy
157 * @param int $maxTrophyIDs maximum number of trophies that are processed
158 * @return int[]
159 * @since 5.2
160 */
161 private function getRevocableUserTrophyIDs(Trophy $trophy, $maxTrophyIDs)
162 {
163 // Unfortunately, the condition system does not support negated conditions.
164 // Therefore, we need to build our own SQL query. To get to the conditions
165 // that must be fulfilled for earning a specific trophy, we first create
166 // a pseudo DBOList element to pass them to the condition handler. Then we
167 // extract the condition builder from the object.
168 $pseudoUserList = new UserList();
169
170 $conditions = $trophy->getConditions();
171
172 // Check if there are conditions for the award of the trophy for the given trophy.
173 // If there are no conditions, we simply return an empty list and do not remove any trophy.
174 // A trophy without conditions that is awarded automatically cannot be created by default.
175 if (empty($conditions)) {
176 return [];
177 }
178
179 // Assign the condition to the pseudo DBOList object
180 foreach ($conditions as $condition) {
181 $condition->getObjectType()->getProcessor()->addUserCondition($condition, $pseudoUserList);
182 }
183
184 // Now we create our own query to find out which users no longer meet the conditions.
185 // For this we use a UserList object again and transfer basic data from the pseudo object.
186 $userList = new UserList();
187 $userList->sqlOrderBy = $pseudoUserList->sqlOrderBy;
188 $userList->sqlLimit = $maxTrophyIDs;
189
190 // Now we copy the sql joins from the pseudo object to the new one if a condition
191 // has joined a table.
192 $userList->sqlJoins = $pseudoUserList->sqlJoins;
193
194 // We joining the user_trophy table to receive the userTrophyID, which should be deleted.
195 $userList->sqlJoins .= " LEFT JOIN wcf" . WCF_N . "_user_trophy user_trophy ON (user_table.userID = user_trophy.userID)";
196
197 // We do not need the complete user object, but only the userTrophyID.
198 // So that the UserList object can also assign the users (which is used
199 // as an array index), we also get the userID.
200 $userList->useQualifiedShorthand = false;
201 $userList->sqlSelects = "user_trophy.userTrophyID, user_table.userID";
202
203 // Now we transfer the old conditions to our new object. To avoid having two WHERE keywords,
204 // we deactivate it in the pseudo-object.
205 $pseudoUserList->getConditionBuilder()->enableWhereKeyword(false);
206 $userList->getConditionBuilder()->add(
207 'NOT(' . $pseudoUserList->getConditionBuilder() . ')',
208 $pseudoUserList->getConditionBuilder()->getParameters()
209 );
210
211 // In order not to get all users who do not fulfill the conditions (in case of
212 // doubt there can be many), we filter for users who have received the trophy.
213 $userList->getConditionBuilder()->add(
214 'user_table.userID IN (SELECT userID FROM wcf' . WCF_N . '_user_trophy WHERE trophyID IN (?))',
215 [$trophy->trophyID]
216 );
217
218 // Prevents us from getting faulty UserTrophyIDs.
219 $userList->getConditionBuilder()->add('user_trophy.trophyID = ?', [$trophy->trophyID]);
220 $userList->readObjects();
221
222 // We now return an array of userTrophyIDs instead of user objects to remove them directly via DBOAction.
223 return \array_map(static function ($object) {
224 return $object->userTrophyID;
225 }, $userList->getObjects());
226 }
227 }