Fixed time zone calculation issue
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / user / UserAction.class.php
1 <?php
2 namespace wcf\data\user;
3 use wcf\data\object\type\ObjectTypeCache;
4 use wcf\data\user\avatar\UserAvatarAction;
5 use wcf\data\user\group\UserGroup;
6 use wcf\data\user\UserEditor;
7 use wcf\data\AbstractDatabaseObjectAction;
8 use wcf\data\IClipboardAction;
9 use wcf\data\ISearchAction;
10 use wcf\system\clipboard\ClipboardHandler;
11 use wcf\system\database\util\PreparedStatementConditionBuilder;
12 use wcf\system\exception\PermissionDeniedException;
13 use wcf\system\exception\UserInputException;
14 use wcf\system\request\RequestHandler;
15 use wcf\system\WCF;
16 use wcf\util\UserRegistrationUtil;
17
18 /**
19 * Executes user-related actions.
20 *
21 * @author Alexander Ebert
22 * @copyright 2001-2014 WoltLab GmbH
23 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
24 * @package com.woltlab.wcf
25 * @subpackage data.user
26 * @category Community Framework
27 */
28 class UserAction extends AbstractDatabaseObjectAction implements IClipboardAction, ISearchAction {
29 /**
30 * @see \wcf\data\AbstractDatabaseObjectAction::$className
31 */
32 public $className = 'wcf\data\user\UserEditor';
33
34 /**
35 * @see \wcf\data\AbstractDatabaseObjectAction::$allowGuestAccess
36 */
37 protected $allowGuestAccess = array('getSearchResultList');
38
39 /**
40 * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsCreate
41 */
42 protected $permissionsCreate = array('admin.user.canAddUser');
43
44 /**
45 * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsDelete
46 */
47 protected $permissionsDelete = array('admin.user.canDeleteUser');
48
49 /**
50 * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsUpdate
51 */
52 protected $permissionsUpdate = array('admin.user.canEditUser');
53
54 /**
55 * @see \wcf\data\AbstractDatabaseObjectAction::$requireACP
56 */
57 protected $requireACP = array('create', 'ban', 'delete', 'disable', 'enable', 'unban');
58
59 /**
60 * Validates permissions and parameters.
61 */
62 public function validateCreate() {
63 $this->readString('password', false, 'data');
64 }
65
66 /**
67 * Validates accessible groups.
68 *
69 * @param boolean $ignoreOwnUser
70 */
71 protected function __validateAccessibleGroups($ignoreOwnUser = true) {
72 if ($ignoreOwnUser) {
73 if (in_array(WCF::getUser()->userID, $this->objectIDs)) {
74 unset($this->objectIDs[array_search(WCF::getUser()->userID, $this->objectIDs)]);
75 if (isset($this->objects[WCF::getUser()->userID])) {
76 unset($this->objects[WCF::getUser()->userID]);
77 }
78 }
79 }
80
81 // list might be empty because only our own user id was given
82 if (empty($this->objectIDs)) {
83 throw new UserInputException('objectIDs');
84 }
85
86 // validate groups
87 $conditions = new PreparedStatementConditionBuilder();
88 $conditions->add("userID IN (?)", array($this->objectIDs));
89
90 $sql = "SELECT DISTINCT groupID
91 FROM wcf".WCF_N."_user_to_group
92 ".$conditions;
93 $statement = WCF::getDB()->prepareStatement($sql);
94 $statement->execute($conditions->getParameters());
95
96 $groupIDs = array();
97 while ($row = $statement->fetchArray()) {
98 $groupIDs[] = $row['groupID'];
99 }
100
101 if (!UserGroup::isAccessibleGroup($groupIDs)) {
102 throw new PermissionDeniedException();
103 }
104 }
105
106 /**
107 * Validates permissions and parameters.
108 */
109 public function validateDelete() {
110 // read and validate user objects
111 parent::validateDelete();
112
113 $this->__validateAccessibleGroups();
114 }
115
116 /**
117 * @see \wcf\data\IDeleteAction::delete()
118 */
119 public function delete() {
120 if (empty($this->objects)) {
121 $this->readObjects();
122 }
123
124 // delete avatars
125 $avatarIDs = array();
126 foreach ($this->objects as $user) {
127 if ($user->avatarID) $avatarIDs[] = $user->avatarID;
128 }
129 if (!empty($avatarIDs)) {
130 $action = new UserAvatarAction($avatarIDs, 'delete');
131 $action->executeAction();
132 }
133
134 // delete profile comments
135 if (!empty($this->objectIDs)) {
136 $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.comment.commentableContent', 'com.woltlab.wcf.user.profileComment');
137 $conditionBuilder = new PreparedStatementConditionBuilder();
138 $conditionBuilder->add('objectTypeID = ?', array($objectType->objectTypeID));
139 $conditionBuilder->add('objectID IN (?)', array($this->objectIDs));
140
141 $sql = "DELETE FROM wcf".WCF_N."_comment
142 ".$conditionBuilder;
143 $statement = WCF::getDB()->prepareStatement($sql);
144 $statement->execute($conditionBuilder->getParameters());
145 }
146
147 $returnValue = parent::delete();
148
149 return $returnValue;
150 }
151
152 /**
153 * Validates permissions and parameters.
154 */
155 public function validateUpdate() {
156 // read objects
157 if (empty($this->objects)) {
158 $this->readObjects();
159
160 if (empty($this->objects)) {
161 throw new UserInputException('objectIDs');
162 }
163 }
164
165 // disallow updating of anything except for options outside of ACP
166 if (RequestHandler::getInstance()->isACPRequest() && (count($this->parameters) != 1 || !isset($this->parameters['options']))) {
167 throw new PermissionDeniedException();
168 }
169
170 try {
171 WCF::getSession()->checkPermissions($this->permissionsUpdate);
172 }
173 catch (PermissionDeniedException $e) {
174 // check if we're editing ourselves
175 if (count($this->objects) == 1 && ($this->objects[0]->userID == WCF::getUser()->userID)) {
176 $count = count($this->parameters);
177 if ($count > 1 || ($count == 1 && !isset($this->parameters['options']))) {
178 throw new PermissionDeniedException();
179 }
180 }
181
182 throw new PermissionDeniedException();
183 }
184 }
185
186 /**
187 * Validates the ban action.
188 */
189 public function validateBan() {
190 WCF::getSession()->checkPermissions(array('admin.user.canBanUser'));
191
192 $this->__validateAccessibleGroups();
193 }
194
195 /**
196 * Validates the unban action.
197 */
198 public function validateUnban() {
199 $this->validateBan();
200 }
201
202 /**
203 * Bans users.
204 */
205 public function ban() {
206 $conditionBuilder = new PreparedStatementConditionBuilder();
207 $conditionBuilder->add('userID IN (?)', array($this->objectIDs));
208 $sql = "UPDATE wcf".WCF_N."_user
209 SET banned = ?,
210 banReason = ?
211 ".$conditionBuilder;
212 $statement = WCF::getDB()->prepareStatement($sql);
213 $statement->execute(
214 array_merge(array(1, $this->parameters['banReason']), $conditionBuilder->getParameters())
215 );
216
217 $this->unmarkItems();
218 }
219
220 /**
221 * Unbans users.
222 */
223 public function unban() {
224 $conditionBuilder = new PreparedStatementConditionBuilder();
225 $conditionBuilder->add('userID IN (?)', array($this->objectIDs));
226 $sql = "UPDATE wcf".WCF_N."_user
227 SET banned = 0
228 ".$conditionBuilder;
229 $statement = WCF::getDB()->prepareStatement($sql);
230 $statement->execute($conditionBuilder->getParameters());
231 }
232
233 /**
234 * Creates a new user.
235 *
236 * @return User
237 */
238 public function create() {
239 $user = parent::create();
240 $userEditor = new UserEditor($user);
241
242 // updates user options
243 if (isset($this->parameters['options'])) {
244 $userEditor->updateUserOptions($this->parameters['options']);
245 }
246
247 // insert user groups
248 $addDefaultGroups = (isset($this->parameters['addDefaultGroups'])) ? $this->parameters['addDefaultGroups'] : true;
249 $groupIDs = (isset($this->parameters['groups'])) ? $this->parameters['groups'] : array();
250 $userEditor->addToGroups($groupIDs, false, $addDefaultGroups);
251
252 // insert visible languages
253 $languageIDs = (isset($this->parameters['languages'])) ? $this->parameters['languages'] : array();
254 $userEditor->addToLanguages($languageIDs, false);
255
256 if (PACKAGE_ID) {
257 // set default notifications
258 $sql = "INSERT INTO wcf".WCF_N."_user_notification_event_to_user
259 (userID, eventID)
260 SELECT ?, eventID
261 FROM wcf".WCF_N."_user_notification_event
262 WHERE preset = ?";
263 $statement = WCF::getDB()->prepareStatement($sql);
264 $statement->execute(array($user->userID, 1));
265
266 // update user rank
267 if (MODULE_USER_RANK) {
268 $action = new UserProfileAction(array($userEditor), 'updateUserRank');
269 $action->executeAction();
270 }
271 // update user online marking
272 $action = new UserProfileAction(array($userEditor), 'updateUserOnlineMarking');
273 $action->executeAction();
274 }
275
276 return $user;
277 }
278
279 /**
280 * @see \wcf\data\AbstractDatabaseObjectAction::update()
281 */
282 public function update() {
283 if (isset($this->parameters['data'])) {
284 parent::update();
285
286 if (isset($this->parameters['data']['languageID'])) {
287 foreach ($this->objects as $object) {
288 if ($object->userID == WCF::getUser()->userID) {
289 if ($this->parameters['data']['languageID'] != WCF::getUser()->languageID) {
290 WCF::setLanguage($this->parameters['data']['languageID']);
291 }
292
293 break;
294 }
295 }
296 }
297 }
298 else {
299 if (empty($this->objects)) {
300 $this->readObjects();
301 }
302 }
303
304 $groupIDs = (isset($this->parameters['groups'])) ? $this->parameters['groups'] : array();
305 $languageIDs = (isset($this->parameters['languageIDs'])) ? $this->parameters['languageIDs'] : array();
306 $removeGroups = (isset($this->parameters['removeGroups'])) ? $this->parameters['removeGroups'] : array();
307 $userOptions = (isset($this->parameters['options'])) ? $this->parameters['options'] : array();
308
309 if (!empty($groupIDs)) {
310 $action = new UserAction($this->objects, 'addToGroups', array(
311 'groups' => $groupIDs,
312 'addDefaultGroups' => false
313 ));
314 $action->executeAction();
315 }
316
317 foreach ($this->objects as $userEditor) {
318 if (!empty($removeGroups)) {
319 $userEditor->removeFromGroups($removeGroups);
320 }
321
322 if (!empty($userOptions)) {
323 $userEditor->updateUserOptions($userOptions);
324 }
325
326 if (!empty($languageIDs)) {
327 $userEditor->addToLanguages($languageIDs);
328 }
329 }
330 }
331
332 /**
333 * Add users to given groups.
334 */
335 public function addToGroups() {
336 if (empty($this->objects)) {
337 $this->readObjects();
338 }
339
340 $groupIDs = $this->parameters['groups'];
341 $deleteOldGroups = $addDefaultGroups = true;
342 if (isset($this->parameters['deleteOldGroups'])) $deleteOldGroups = $this->parameters['deleteOldGroups'];
343 if (isset($this->parameters['addDefaultGroups'])) $addDefaultGroups = $this->parameters['addDefaultGroups'];
344
345 foreach ($this->objects as $userEditor) {
346 $userEditor->addToGroups($groupIDs, $deleteOldGroups, $addDefaultGroups);
347 }
348
349 //reread objects
350 $this->objects = array();
351 UserEditor::resetCache();
352 $this->readObjects();
353
354 if (MODULE_USER_RANK) {
355 $action = new UserProfileAction($this->objects, 'updateUserRank');
356 $action->executeAction();
357 }
358 if (MODULE_USERS_ONLINE) {
359 $action = new UserProfileAction($this->objects, 'updateUserOnlineMarking');
360 $action->executeAction();
361 }
362 }
363
364 /**
365 * @see \wcf\data\ISearchAction::validateGetSearchResultList()
366 */
367 public function validateGetSearchResultList() {
368 $this->readBoolean('includeUserGroups', false, 'data');
369 $this->readString('searchString', false, 'data');
370
371 if (isset($this->parameters['data']['excludedSearchValues']) && !is_array($this->parameters['data']['excludedSearchValues'])) {
372 throw new UserInputException('excludedSearchValues');
373 }
374 }
375
376 /**
377 * @see \wcf\data\ISearchAction::getSearchResultList()
378 */
379 public function getSearchResultList() {
380 $searchString = $this->parameters['data']['searchString'];
381 $excludedSearchValues = array();
382 if (isset($this->parameters['data']['excludedSearchValues'])) {
383 $excludedSearchValues = $this->parameters['data']['excludedSearchValues'];
384 }
385 $list = array();
386
387 if ($this->parameters['data']['includeUserGroups']) {
388 $accessibleGroups = UserGroup::getAccessibleGroups();
389 foreach ($accessibleGroups as $group) {
390 $groupName = $group->getName();
391 if (!in_array($groupName, $excludedSearchValues)) {
392 $pos = mb_strripos($groupName, $searchString);
393 if ($pos !== false && $pos == 0) {
394 $list[] = array(
395 'label' => $groupName,
396 'objectID' => $group->groupID,
397 'type' => 'group'
398 );
399 }
400 }
401 }
402 }
403
404 // find users
405 $userProfileList = new UserProfileList();
406 $userProfileList->getConditionBuilder()->add("username LIKE ?", array($searchString.'%'));
407 if (!empty($excludedSearchValues)) {
408 $userProfileList->getConditionBuilder()->add("username NOT IN (?)", array($excludedSearchValues));
409 }
410 $userProfileList->sqlLimit = 10;
411 $userProfileList->readObjects();
412
413 foreach ($userProfileList as $userProfile) {
414 $list[] = array(
415 'icon' => $userProfile->getAvatar()->getImageTag(16),
416 'label' => $userProfile->username,
417 'objectID' => $userProfile->userID,
418 'type' => 'user'
419 );
420 }
421
422 return $list;
423 }
424
425 /**
426 * @see \wcf\data\IClipboardAction::validateUnmarkAll()
427 */
428 public function validateUnmarkAll() {
429 // does nothing
430 }
431
432 /**
433 * @see \wcf\data\IClipboardAction::unmarkAll()
434 */
435 public function unmarkAll() {
436 ClipboardHandler::getInstance()->removeItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.user'));
437 }
438
439 /**
440 * Unmarks users.
441 *
442 * @param array<integer> $userIDs
443 */
444 protected function unmarkItems(array $userIDs = array()) {
445 if (empty($userIDs)) {
446 $userIDs = $this->objectIDs;
447 }
448
449 if (!empty($userIDs)) {
450 ClipboardHandler::getInstance()->unmark($userIDs, ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.user'));
451 }
452 }
453
454 /**
455 * Validates the enable action.
456 */
457 public function validateEnable() {
458 WCF::getSession()->checkPermissions(array('admin.user.canEnableUser'));
459
460 $this->__validateAccessibleGroups();
461 }
462
463 /**
464 * Validates the disable action.
465 */
466 public function validateDisable() {
467 $this->validateEnable();
468 }
469
470 /**
471 * Enables users.
472 */
473 public function enable() {
474 if (empty($this->objects)) $this->readObjects();
475
476 $action = new UserAction($this->objects, 'update', array(
477 'data' => array(
478 'activationCode' => 0
479 ),
480 'removeGroups' => UserGroup::getGroupIDsByType(array(UserGroup::GUESTS))
481 ));
482 $action->executeAction();
483 $action = new UserAction($this->objects, 'addToGroups', array(
484 'groups' => UserGroup::getGroupIDsByType(array(UserGroup::USERS)),
485 'deleteOldGroups' => false,
486 'addDefaultGroups' => false
487 ));
488 $action->executeAction();
489
490 $this->unmarkItems();
491 }
492
493 /**
494 * Disables users.
495 */
496 public function disable() {
497 if (empty($this->objects)) $this->readObjects();
498
499 $action = new UserAction($this->objects, 'update', array(
500 'data' => array(
501 'activationCode' => UserRegistrationUtil::getActivationCode()
502 ),
503 'removeGroups' => UserGroup::getGroupIDsByType(array(UserGroup::USERS)),
504 ));
505 $action->executeAction();
506 $action = new UserAction($this->objects, 'addToGroups', array(
507 'groups' => UserGroup::getGroupIDsByType(array(UserGroup::GUESTS)),
508 'deleteOldGroups' => false,
509 'addDefaultGroups' => false
510 ));
511 $action->executeAction();
512
513 $this->unmarkItems();
514 }
515
516 /**
517 * @see \wcf\data\AbstractDatabaseObjectAction::readObjects()
518 */
519 protected function readObjects() {
520 if (empty($this->objectIDs)) {
521 return;
522 }
523
524 // get base class
525 $baseClass = call_user_func(array($this->className, 'getBaseClass'));
526
527 // get objects
528 $sql = "SELECT user_option_value.*, user_table.*
529 FROM wcf".WCF_N."_user user_table
530 LEFT JOIN wcf".WCF_N."_user_option_value user_option_value
531 ON (user_option_value.userID = user_table.userID)
532 WHERE user_table.userID IN (".str_repeat('?,', count($this->objectIDs) - 1)."?)";
533 $statement = WCF::getDB()->prepareStatement($sql);
534 $statement->execute($this->objectIDs);
535 while ($object = $statement->fetchObject($baseClass)) {
536 $this->objects[] = new $this->className($object);
537 }
538 }
539 }