Merge branch '5.3'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / acp / page / UserListPage.class.php
1 <?php
2
3 namespace wcf\acp\page;
4
5 use wcf\data\user\group\UserGroup;
6 use wcf\data\user\option\ViewableUserOption;
7 use wcf\data\user\User;
8 use wcf\data\user\UserProfile;
9 use wcf\page\SortablePage;
10 use wcf\system\cache\builder\UserOptionCacheBuilder;
11 use wcf\system\clipboard\ClipboardHandler;
12 use wcf\system\database\util\PreparedStatementConditionBuilder;
13 use wcf\system\event\EventHandler;
14 use wcf\system\exception\IllegalLinkException;
15 use wcf\system\option\IOptionHandler;
16 use wcf\system\option\user\IUserOptionOutput;
17 use wcf\system\request\LinkHandler;
18 use wcf\system\WCF;
19 use wcf\util\DateUtil;
20 use wcf\util\StringUtil;
21
22 /**
23 * Shows the result of a user search.
24 *
25 * @author Marcel Werk
26 * @copyright 2001-2019 WoltLab GmbH
27 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
28 * @package WoltLabSuite\Core\Acp\Page
29 */
30 class UserListPage extends SortablePage
31 {
32 /**
33 * list of displayed column names
34 * @var string[]
35 */
36 public $columnHeads = [];
37
38 /**
39 * list of selected columns
40 * @var string[]
41 */
42 public $columns = ['registrationDate', 'lastActivityTime'];
43
44 /**
45 * applies special CSS classes for selected columns
46 * @var array
47 */
48 public $columnStyling = [
49 'registrationDate' => 'columnDate',
50 'lastActivityTime' => 'columnDate',
51 'profileHits' => 'columnDigits',
52 'activityPoints' => 'columnDigits',
53 'likesReceived' => 'columnDigits',
54 ];
55
56 /**
57 * list of column values
58 * @var string[][]
59 */
60 public $columnValues = [];
61
62 /**
63 * @inheritDoc
64 */
65 public $defaultSortField = 'username';
66
67 /**
68 * @inheritDoc
69 */
70 public $itemsPerPage = 50;
71
72 /**
73 * list of marked user ids
74 * @var int[]
75 */
76 public $markedUsers = [];
77
78 /**
79 * @inheritDoc
80 */
81 public $neededPermissions = ['admin.user.canSearchUser'];
82
83 /**
84 * IOptionHandler object
85 * @var IOptionHandler
86 */
87 protected $optionHandler;
88
89 /**
90 * list of available user option names
91 * @var array
92 */
93 public $options = [];
94
95 /**
96 * id of a user search
97 * @var int
98 */
99 public $searchID = 0;
100
101 /**
102 * list of user ids
103 * @var int[]
104 */
105 public $userIDs = [];
106
107 /**
108 * list of users
109 * @var UserProfile[]
110 */
111 public $users = [];
112
113 /**
114 * page url
115 * @var string
116 */
117 public $url = '';
118
119 /**
120 * condition builder for user filtering
121 * @var PreparedStatementConditionBuilder
122 */
123 public $conditions;
124
125 /**
126 * @inheritDoc
127 */
128 public $validSortFields = [
129 'userID',
130 'registrationDate',
131 'username',
132 'lastActivityTime',
133 'profileHits',
134 'activityPoints',
135 'likesReceived',
136 ];
137
138 /**
139 * @inheritDoc
140 */
141 public function readParameters()
142 {
143 parent::readParameters();
144
145 $this->conditions = new PreparedStatementConditionBuilder();
146
147 if (!empty($_REQUEST['id'])) {
148 $this->searchID = \intval($_REQUEST['id']);
149 if ($this->searchID) {
150 $this->readSearchResult();
151 }
152 if (empty($this->userIDs)) {
153 throw new IllegalLinkException();
154 }
155 $this->conditions->add("user_table.userID IN (?)", [$this->userIDs]);
156 }
157
158 // get user options
159 $this->readUserOptions();
160 }
161
162 /**
163 * @inheritDoc
164 */
165 public function validateSortField()
166 {
167 // add options to valid sort fields
168 $this->validSortFields = \array_merge($this->validSortFields, \array_keys($this->options));
169
170 // avoid leaking mail adresses by sorting
171 if (WCF::getSession()->getPermission('admin.user.canEditMailAddress')) {
172 $this->validSortFields[] = 'email';
173 }
174
175 parent::validateSortField();
176 }
177
178 /**
179 * @inheritDoc
180 */
181 public function readData()
182 {
183 parent::readData();
184
185 // add email column for authorized users
186 if (WCF::getSession()->getPermission('admin.user.canEditMailAddress')) {
187 \array_unshift($this->columns, 'email');
188 }
189
190 // get marked users
191 $this->markedUsers = WCF::getSession()->getVar('markedUsers');
192 if ($this->markedUsers == null || !\is_array($this->markedUsers)) {
193 $this->markedUsers = [];
194 }
195
196 // get columns heads
197 $this->readColumnsHeads();
198
199 // get users
200 $this->readUsers();
201
202 // build page url
203 $this->url = LinkHandler::getInstance()->getLink(
204 'UserList',
205 [],
206 'searchID=' . $this->searchID . '&action=' . \rawurlencode($this->action) . '&pageNo=' . $this->pageNo . '&sortField=' . $this->sortField . '&sortOrder=' . $this->sortOrder
207 );
208 }
209
210 /**
211 * @inheritDoc
212 */
213 public function assignVariables()
214 {
215 parent::assignVariables();
216
217 WCF::getTPL()->assign([
218 'users' => $this->users,
219 'searchID' => $this->searchID,
220 'hasMarkedItems' => ClipboardHandler::getInstance()->hasMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.user')),
221 'url' => $this->url,
222 'columnHeads' => $this->columnHeads,
223 'columnValues' => $this->columnValues,
224 'columnStyling' => $this->columnStyling,
225 ]);
226 }
227
228 /**
229 * @inheritDoc
230 */
231 public function show()
232 {
233 $this->activeMenuItem = 'wcf.acp.menu.link.user.' . ($this->searchID ? 'search' : 'list');
234
235 parent::show();
236 }
237
238 /**
239 * @inheritDoc
240 */
241 public function countItems()
242 {
243 // call countItems event
244 EventHandler::getInstance()->fireAction($this, 'countItems');
245
246 $sql = "SELECT COUNT(*)
247 FROM wcf" . WCF_N . "_user user_table
248 " . $this->conditions;
249 $statement = WCF::getDB()->prepareStatement($sql);
250 $statement->execute($this->conditions->getParameters());
251
252 return $statement->fetchSingleColumn();
253 }
254
255 /**
256 * Fetches the list of results.
257 */
258 protected function readUsers()
259 {
260 // get user ids
261 $sql = "SELECT user_table.userID
262 FROM wcf" . WCF_N . "_user user_table
263 " . (isset($this->options[$this->sortField]) ? "LEFT JOIN wcf" . WCF_N . "_user_option_value user_option_value ON (user_option_value.userID = user_table.userID)" : '') . "
264 " . $this->conditions . "
265 ORDER BY " . (($this->sortField != 'email' && isset($this->options[$this->sortField])) ? 'user_option_value.userOption' . $this->options[$this->sortField]->optionID : $this->sortField) . " " . $this->sortOrder;
266 $statement = WCF::getDB()->prepareStatement(
267 $sql,
268 $this->itemsPerPage,
269 ($this->pageNo - 1) * $this->itemsPerPage
270 );
271 $statement->execute($this->conditions->getParameters());
272 $userIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
273
274 // get user data
275 if (!empty($userIDs)) {
276 // get group ids
277 $conditions = new PreparedStatementConditionBuilder();
278 $conditions->add("user_table.userID IN (?)", [$userIDs]);
279
280 $sql = "SELECT userID, groupID
281 FROM wcf" . WCF_N . "_user_to_group user_table
282 " . $conditions;
283 $statement = WCF::getDB()->prepareStatement($sql);
284 $statement->execute($conditions->getParameters());
285 $userToGroups = $statement->fetchMap('userID', 'groupID', false);
286
287 $sql = "SELECT user_avatar.*, option_value.*, user_table.*
288 FROM wcf" . WCF_N . "_user user_table
289 LEFT JOIN wcf" . WCF_N . "_user_option_value option_value
290 ON (option_value.userID = user_table.userID)
291 LEFT JOIN wcf" . WCF_N . "_user_avatar user_avatar
292 ON (user_avatar.avatarID = user_table.avatarID)
293 " . $conditions . "
294 ORDER BY " . (($this->sortField != 'email' && isset($this->options[$this->sortField])) ? 'option_value.userOption' . $this->options[$this->sortField]->optionID : 'user_table.' . $this->sortField) . " " . $this->sortOrder;
295 $statement = WCF::getDB()->prepareStatement($sql);
296 $statement->execute($conditions->getParameters());
297 while ($row = $statement->fetchArray()) {
298 $groupIDs = ($userToGroups[$row['userID']] ?? []);
299
300 $row['groupIDs'] = \implode(',', $groupIDs);
301 $accessible = (!empty($groupIDs) ? UserGroup::isAccessibleGroup($groupIDs) : true);
302 $row['accessible'] = $accessible;
303 $row['deletable'] = ($accessible && WCF::getSession()->getPermission('admin.user.canDeleteUser') && $row['userID'] != WCF::getUser()->userID) ? 1 : 0;
304 $row['editable'] = ($accessible && WCF::getSession()->getPermission('admin.user.canEditUser')) ? 1 : 0;
305 $row['bannable'] = ($accessible && WCF::getSession()->getPermission('admin.user.canBanUser') && $row['userID'] != WCF::getUser()->userID) ? 1 : 0;
306 $row['canBeEnabled'] = ($accessible && WCF::getSession()->getPermission('admin.user.canEnableUser') && $row['userID'] != WCF::getUser()->userID) ? 1 : 0;
307 $row['isMarked'] = \intval(\in_array($row['userID'], $this->markedUsers));
308
309 $this->users[] = new UserProfile(new User(null, $row));
310 }
311
312 // get special columns
313 foreach ($this->users as $key => $user) {
314 foreach ($this->columns as $column) {
315 switch ($column) {
316 case 'email':
317 $this->columnValues[$user->userID][$column] = '<a href="mailto:' . StringUtil::encodeHTML($user->email) . '">' . StringUtil::encodeHTML($user->email) . '</a>';
318 break;
319
320 case 'registrationDate':
321 $this->columnValues[$user->userID][$column] = DateUtil::format(
322 DateUtil::getDateTimeByTimestamp($user->{$column}),
323 DateUtil::DATE_FORMAT
324 );
325 break;
326
327 case 'lastActivityTime':
328 if ($user->{$column}) {
329 $this->columnValues[$user->userID][$column] = \str_replace(
330 '%time%',
331 DateUtil::format(
332 DateUtil::getDateTimeByTimestamp($user->{$column}),
333 DateUtil::TIME_FORMAT
334 ),
335 \str_replace(
336 '%date%',
337 DateUtil::format(
338 DateUtil::getDateTimeByTimestamp($user->{$column}),
339 DateUtil::DATE_FORMAT
340 ),
341 WCF::getLanguage()->get('wcf.date.dateTimeFormat')
342 )
343 );
344 }
345 break;
346
347 case 'profileHits':
348 case 'activityPoints':
349 case 'likesReceived':
350 $this->columnValues[$user->userID][$column] = StringUtil::formatInteger($user->{$column});
351 break;
352
353 default:
354 if (isset($this->options[$column])) {
355 if ($this->options[$column]->outputClass) {
356 $this->options[$column]->setOptionValue($user->getDecoratedObject());
357
358 /** @var IUserOptionOutput $outputObj */
359 $outputObj = $this->options[$column]->getOutputObject();
360 $this->columnValues[$user->userID][$column] = $outputObj->getOutput(
361 $user->getDecoratedObject(),
362 $this->options[$column]->getDecoratedObject(),
363 $user->{$column}
364 );
365 } else {
366 $this->columnValues[$user->userID][$column] = StringUtil::encodeHTML($user->{$column});
367 }
368 }
369 break;
370 }
371 }
372 }
373 }
374 }
375
376 /**
377 * Fetches the result of the search with the given search id.
378 */
379 protected function readSearchResult()
380 {
381 // get user search from database
382 $sql = "SELECT searchData
383 FROM wcf" . WCF_N . "_search
384 WHERE searchID = ?
385 AND userID = ?
386 AND searchType = ?";
387 $statement = WCF::getDB()->prepareStatement($sql);
388 $statement->execute([
389 $this->searchID,
390 WCF::getUser()->userID,
391 'users',
392 ]);
393 $search = $statement->fetchArray();
394 if (!isset($search['searchData'])) {
395 throw new IllegalLinkException();
396 }
397
398 $data = \unserialize($search['searchData']);
399 $this->userIDs = $data['matches'];
400 $this->itemsPerPage = $data['itemsPerPage'];
401 $this->columns = $data['columns'];
402 unset($data);
403 }
404
405 /**
406 * Fetches the user options from cache.
407 */
408 protected function readUserOptions()
409 {
410 $this->options = UserOptionCacheBuilder::getInstance()->getData([], 'options');
411
412 foreach ($this->options as &$option) {
413 $option = new ViewableUserOption($option);
414 }
415 unset($option);
416 }
417
418 /**
419 * Reads the column heads.
420 */
421 protected function readColumnsHeads()
422 {
423 foreach ($this->columns as $column) {
424 if ($column == 'likesReceived') {
425 $this->columnHeads[$column] = 'wcf.like.likesReceived';
426 continue;
427 }
428 if ($column == 'activityPoints') {
429 $this->columnHeads[$column] = 'wcf.user.activityPoint';
430 continue;
431 }
432
433 if (isset($this->options[$column]) && $column != 'email') {
434 $this->columnHeads[$column] = 'wcf.user.option.' . $column;
435 } else {
436 $this->columnHeads[$column] = 'wcf.user.' . $column;
437 }
438 }
439 }
440
441 /**
442 * @inheritDoc
443 */
444 protected function initObjectList()
445 {
446 // does nothing
447 }
448
449 /**
450 * @inheritDoc
451 */
452 protected function readObjects()
453 {
454 // does nothing
455 }
456 }