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