elkarte /
Elkarte
| 1 | <?php |
||
| 2 | |||
| 3 | /** |
||
| 4 | * Show a list of members or a selection of members. |
||
| 5 | * |
||
| 6 | * @package ElkArte Forum |
||
| 7 | * @copyright ElkArte Forum contributors |
||
| 8 | * @license BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file) |
||
| 9 | * |
||
| 10 | * This file contains code covered by: |
||
| 11 | * copyright: 2011 Simple Machines (http://www.simplemachines.org) |
||
| 12 | * |
||
| 13 | * @version 2.0 dev |
||
| 14 | * |
||
| 15 | */ |
||
| 16 | |||
| 17 | namespace ElkArte\AdminController; |
||
| 18 | |||
| 19 | use ElkArte\AbstractController; |
||
| 20 | use ElkArte\Action; |
||
| 21 | use ElkArte\Cache\Cache; |
||
| 22 | use ElkArte\Helper\Util; |
||
| 23 | use ElkArte\Languages\Txt; |
||
| 24 | use ElkArte\User; |
||
| 25 | |||
| 26 | /** |
||
| 27 | * ManageMembers controller deals with members administration, approval, |
||
| 28 | * admin-visible list and search in it. |
||
| 29 | * |
||
| 30 | * @package Members |
||
| 31 | */ |
||
| 32 | class ManageMembers extends AbstractController |
||
| 33 | { |
||
| 34 | /** @var array Holds various setting conditions for the current action */ |
||
| 35 | protected array $conditions; |
||
| 36 | |||
| 37 | /** @var array Holds the members that the action is being applied to */ |
||
| 38 | protected array $member_info; |
||
| 39 | |||
| 40 | /** |
||
| 41 | * The main entrance point for the Manage Members screen. |
||
| 42 | * |
||
| 43 | * What it does: |
||
| 44 | * |
||
| 45 | * - As everywhere else, it calls a function based on the given sub-action. |
||
| 46 | * - Called by ?action=admin;area=viewmembers. |
||
| 47 | * - Requires the moderate_forum permission. |
||
| 48 | * |
||
| 49 | * @event integrate_manage_members used to add subactions and tabs |
||
| 50 | * @uses ManageMembers template |
||
| 51 | * @uses ManageMembers language file. |
||
| 52 | * @see AbstractCowntroller::action_index() |
||
| 53 | */ |
||
| 54 | public function action_index() |
||
| 55 | { |
||
| 56 | global $txt, $context, $modSettings; |
||
| 57 | |||
| 58 | // Load the essentials. |
||
| 59 | Txt::load('ManageMembers'); |
||
| 60 | theme()->getTemplates()->load('ManageMembers'); |
||
| 61 | |||
| 62 | $subActions = [ |
||
| 63 | 'all' => [ |
||
| 64 | 'controller' => $this, |
||
| 65 | 'function' => 'action_list', |
||
| 66 | 'permission' => 'moderate_forum'], |
||
| 67 | 'approve' => [ |
||
| 68 | 'controller' => $this, |
||
| 69 | 'function' => 'action_approve', |
||
| 70 | 'permission' => 'moderate_forum'], |
||
| 71 | 'browse' => [ |
||
| 72 | 'controller' => $this, |
||
| 73 | 'function' => 'action_browse', |
||
| 74 | 'permission' => 'moderate_forum'], |
||
| 75 | 'search' => [ |
||
| 76 | 'controller' => $this, |
||
| 77 | 'function' => 'action_search', |
||
| 78 | 'permission' => 'moderate_forum'], |
||
| 79 | 'query' => [ |
||
| 80 | 'controller' => $this, |
||
| 81 | 'function' => 'action_list', |
||
| 82 | 'permission' => 'moderate_forum'], |
||
| 83 | ]; |
||
| 84 | |||
| 85 | // Prepare our action control |
||
| 86 | $action = new Action(); |
||
| 87 | |||
| 88 | // Default to sub action 'all', needed for the tabs array below |
||
| 89 | $subAction = $action->initialize($subActions, 'all'); |
||
| 90 | |||
| 91 | // Get counts on every type of activation - for sections and filtering alike. |
||
| 92 | require_once(SUBSDIR . '/Members.subs.php'); |
||
| 93 | |||
| 94 | $context['awaiting_activation'] = 0; |
||
| 95 | $context['awaiting_approval'] = 0; |
||
| 96 | $context['activation_numbers'] = countInactiveMembers(); |
||
| 97 | |||
| 98 | foreach ($context['activation_numbers'] as $activation_type => $total_members) |
||
| 99 | { |
||
| 100 | if (in_array($activation_type, [0, 2], true)) |
||
| 101 | { |
||
| 102 | $context['awaiting_activation'] += $total_members; |
||
| 103 | } |
||
| 104 | elseif (in_array($activation_type, [3, 4, 5], true)) |
||
| 105 | { |
||
| 106 | $context['awaiting_approval'] += $total_members; |
||
| 107 | } |
||
| 108 | } |
||
| 109 | |||
| 110 | // Last items for the template |
||
| 111 | $context['page_title'] = $txt['admin_members']; |
||
| 112 | $context['sub_action'] = $subAction; |
||
| 113 | |||
| 114 | // For the page header... do we show activation? |
||
| 115 | $context['show_activate'] = (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1) || !empty($context['awaiting_activation']); |
||
| 116 | |||
| 117 | // What about approval? |
||
| 118 | $context['show_approve'] = (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 2) || !empty($context['awaiting_approval']) || !empty($modSettings['approveAccountDeletion']); |
||
| 119 | |||
| 120 | // Setup the admin tabs. |
||
| 121 | $context[$context['admin_menu_name']]['object']->prepareTabData([ |
||
| 122 | 'title' => 'admin_members', |
||
| 123 | 'help' => 'view_members', |
||
| 124 | 'description' => 'admin_members_list', |
||
| 125 | 'tabs' => [ |
||
| 126 | 'viewmembers' => [ |
||
| 127 | 'label' => $txt['view_all_members'], |
||
| 128 | 'description' => $txt['admin_members_list'], |
||
| 129 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'viewmembers', 'sa' => 'all']), |
||
| 130 | 'selected' => $subAction === 'all', |
||
| 131 | ], |
||
| 132 | 'search' => [ |
||
| 133 | 'label' => $txt['mlist_search'], |
||
| 134 | 'description' => $txt['admin_members_list'], |
||
| 135 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'viewmembers', 'sa' => 'search']), |
||
| 136 | 'selected' => $subAction === 'search' || $subAction === 'query', |
||
| 137 | ], |
||
| 138 | 'approve' => [ |
||
| 139 | 'label' => sprintf($txt['admin_browse_awaiting_approval'], $context['awaiting_approval']), |
||
| 140 | 'description' => $txt['admin_browse_approve_desc'], |
||
| 141 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'viewmembers', 'sa' => 'browse', 'type' => 'approve']), |
||
| 142 | 'disabled' => !$context['show_approve'] && ($subAction !== 'browse' || $this->_req->getQuery('type') !== 'approve'), |
||
| 143 | 'selected' => $subAction === 'browse' && $this->_req->getQuery('type') === 'approve', |
||
| 144 | ], |
||
| 145 | 'activate' => [ |
||
| 146 | 'label' => sprintf($txt['admin_browse_awaiting_activate'], $context['awaiting_activation']), |
||
| 147 | 'description' => $txt['admin_browse_activate_desc'], |
||
| 148 | 'url' => getUrl('admin', ['action' => 'admin', 'area' => 'viewmembers', 'sa' => 'browse', 'type' => 'activate']), |
||
| 149 | 'disabled' => !$context['show_activate'] && ($subAction !== 'browse' || $this->_req->query->type !== 'activate'), |
||
| 150 | 'selected' => $subAction === 'browse' && $this->_req->getQuery('type') === 'activate', |
||
| 151 | ], |
||
| 152 | ] |
||
| 153 | ]); |
||
| 154 | |||
| 155 | // Call integrate_manage_members |
||
| 156 | call_integration_hook('integrate_manage_members', [&$subActions]); |
||
| 157 | |||
| 158 | // Off we go |
||
| 159 | $action->dispatch($subAction); |
||
| 160 | } |
||
| 161 | |||
| 162 | /** |
||
| 163 | * View all members list. It allows sorting on several columns, and deletion of |
||
| 164 | * selected members. |
||
| 165 | * |
||
| 166 | * - It also handles the search query sent by ?action=admin;area=viewmembers;sa=search. |
||
| 167 | * - Called by ?action=admin;area=viewmembers;sa=all or ?action=admin;area=viewmembers;sa=query. |
||
| 168 | * - Requires the moderate_forum permission. |
||
| 169 | * |
||
| 170 | * @event integrate_list_member_list |
||
| 171 | * @event integrate_view_members_params passed $params |
||
| 172 | * @uses the view_members sub template of the ManageMembers template. |
||
| 173 | */ |
||
| 174 | public function action_list(): void |
||
| 175 | { |
||
| 176 | global $txt, $context, $modSettings; |
||
| 177 | |||
| 178 | // Set the current sub action. |
||
| 179 | $context['sub_action'] = $this->_req->getQuery('sa', 'strval', $this->_req->getPost('sa', 'strval', 'all')); |
||
| 180 | |||
| 181 | // Are we performing a mass action? |
||
| 182 | if (isset($this->_req->post->maction_on_members, $this->_req->post->maction) && !empty($this->_req->post->members)) |
||
| 183 | { |
||
| 184 | $this->_multiMembersAction(); |
||
| 185 | } |
||
| 186 | |||
| 187 | // Check input after a member search has been submitted. |
||
| 188 | if ($context['sub_action'] === 'query') |
||
| 189 | { |
||
| 190 | // Retrieving the membergroups and postgroups. |
||
| 191 | require_once(SUBSDIR . '/Membergroups.subs.php'); |
||
| 192 | $groups = getBasicMembergroupData([], ['moderator'], null, true); |
||
| 193 | |||
| 194 | $context['membergroups'] = $groups['membergroups']; |
||
| 195 | $context['postgroups'] = $groups['groups']; |
||
| 196 | unset($groups); |
||
| 197 | |||
| 198 | // Some data about the form fields and how they are linked to the database. |
||
| 199 | $params = [ |
||
| 200 | 'mem_id' => [ |
||
| 201 | 'db_fields' => ['id_member'], |
||
| 202 | 'type' => 'int', |
||
| 203 | 'range' => true |
||
| 204 | ], |
||
| 205 | 'age' => [ |
||
| 206 | 'db_fields' => ['birthdate'], |
||
| 207 | 'type' => 'age', |
||
| 208 | 'range' => true |
||
| 209 | ], |
||
| 210 | 'posts' => [ |
||
| 211 | 'db_fields' => ['posts'], |
||
| 212 | 'type' => 'int', |
||
| 213 | 'range' => true |
||
| 214 | ], |
||
| 215 | 'reg_date' => [ |
||
| 216 | 'db_fields' => ['date_registered'], |
||
| 217 | 'type' => 'date', |
||
| 218 | 'range' => true |
||
| 219 | ], |
||
| 220 | 'last_online' => [ |
||
| 221 | 'db_fields' => ['last_login'], |
||
| 222 | 'type' => 'date', |
||
| 223 | 'range' => true |
||
| 224 | ], |
||
| 225 | 'activated' => [ |
||
| 226 | 'db_fields' => ['is_activated'], |
||
| 227 | 'type' => 'checkbox', |
||
| 228 | 'values' => ['0', '1', '11'], |
||
| 229 | ], |
||
| 230 | 'membername' => [ |
||
| 231 | 'db_fields' => ['member_name', 'real_name'], |
||
| 232 | 'type' => 'string' |
||
| 233 | ], |
||
| 234 | 'email' => [ |
||
| 235 | 'db_fields' => ['email_address'], |
||
| 236 | 'type' => 'string' |
||
| 237 | ], |
||
| 238 | 'website' => [ |
||
| 239 | 'db_fields' => ['website_title', 'website_url'], |
||
| 240 | 'type' => 'string' |
||
| 241 | ], |
||
| 242 | 'ip' => [ |
||
| 243 | 'db_fields' => ['member_ip'], |
||
| 244 | 'type' => 'string' |
||
| 245 | ] |
||
| 246 | ]; |
||
| 247 | |||
| 248 | $range_trans = [ |
||
| 249 | '--' => '<', |
||
| 250 | '-' => '<=', |
||
| 251 | '=' => '=', |
||
| 252 | '+' => '>=', |
||
| 253 | '++' => '>' |
||
| 254 | ]; |
||
| 255 | |||
| 256 | call_integration_hook('integrate_view_members_params', [&$params]); |
||
| 257 | |||
| 258 | $search_params = []; |
||
| 259 | if ($context['sub_action'] === 'query' && !empty($this->_req->query->params) && empty($this->_req->post->types)) |
||
| 260 | { |
||
| 261 | $search_params = @json_decode(base64_decode($this->_req->query->params), true); |
||
| 262 | } |
||
| 263 | elseif (!empty($this->_req->post)) |
||
| 264 | { |
||
| 265 | $search_params['types'] = $this->_req->post->types; |
||
| 266 | foreach (array_keys($params) as $param_name) |
||
| 267 | { |
||
| 268 | if (isset($this->_req->post->{$param_name})) |
||
| 269 | { |
||
| 270 | $search_params[$param_name] = $this->_req->post->{$param_name}; |
||
| 271 | } |
||
| 272 | } |
||
| 273 | } |
||
| 274 | |||
| 275 | $search_url_params = isset($search_params) ? base64_encode(json_encode($search_params)) : null; |
||
| 276 | |||
| 277 | // @todo Validate a little more. |
||
| 278 | // Loop through every field of the form. |
||
| 279 | $query_parts = []; |
||
| 280 | $where_params = []; |
||
| 281 | foreach ($params as $param_name => $param_info) |
||
| 282 | { |
||
| 283 | // Not filled in? |
||
| 284 | if (!isset($search_params[$param_name]) || $search_params[$param_name] === '') |
||
| 285 | { |
||
| 286 | continue; |
||
| 287 | } |
||
| 288 | |||
| 289 | // Make sure numeric values are really numeric. |
||
| 290 | if (in_array($param_info['type'], ['int', 'age'])) |
||
| 291 | { |
||
| 292 | $search_params[$param_name] = (int) $search_params[$param_name]; |
||
| 293 | } |
||
| 294 | // Date values have to match the specified format. |
||
| 295 | elseif ($param_info['type'] === 'date') |
||
| 296 | { |
||
| 297 | // Check if this date format is valid. |
||
| 298 | if (preg_match('/^\d{4}-\d{1,2}-\d{1,2}$/', $search_params[$param_name]) == 0) |
||
| 299 | { |
||
| 300 | continue; |
||
| 301 | } |
||
| 302 | |||
| 303 | $search_params[$param_name] = strtotime($search_params[$param_name]); |
||
| 304 | } |
||
| 305 | |||
| 306 | // Those values that are in some kind of range (<, <=, =, >=, >). |
||
| 307 | if (!empty($param_info['range'])) |
||
| 308 | { |
||
| 309 | // Default to '=', just in case... |
||
| 310 | if (empty($range_trans[$search_params['types'][$param_name]])) |
||
| 311 | { |
||
| 312 | $search_params['types'][$param_name] = '='; |
||
| 313 | } |
||
| 314 | |||
| 315 | // Handle special case 'age'. |
||
| 316 | if ($param_info['type'] === 'age') |
||
| 317 | { |
||
| 318 | // All people that were born between $lowerlimit and $upperlimit are currently the specified age. |
||
| 319 | $datearray = getdate(forum_time()); |
||
| 320 | $upperlimit = sprintf('%04d-%02d-%02d', $datearray['year'] - $search_params[$param_name], $datearray['mon'], $datearray['mday']); |
||
| 321 | $lowerlimit = sprintf('%04d-%02d-%02d', $datearray['year'] - $search_params[$param_name] - 1, $datearray['mon'], $datearray['mday']); |
||
| 322 | |||
| 323 | if (in_array($search_params['types'][$param_name], ['-', '--', '='])) |
||
| 324 | { |
||
| 325 | $query_parts[] = ($param_info['db_fields'][0]) . ' > {string:' . $param_name . '_minlimit}'; |
||
| 326 | $where_params[$param_name . '_minlimit'] = ($search_params['types'][$param_name] === '--' ? $upperlimit : $lowerlimit); |
||
| 327 | } |
||
| 328 | |||
| 329 | if (in_array($search_params['types'][$param_name], ['+', '++', '='])) |
||
| 330 | { |
||
| 331 | $query_parts[] = ($param_info['db_fields'][0]) . ' <= {string:' . $param_name . '_pluslimit}'; |
||
| 332 | $where_params[$param_name . '_pluslimit'] = ($search_params['types'][$param_name] === '++' ? $lowerlimit : $upperlimit); |
||
| 333 | |||
| 334 | // Make sure that members that didn't set their birth year are not queried. |
||
| 335 | $query_parts[] = ($param_info['db_fields'][0]) . ' > {date:dec_zero_date}'; |
||
| 336 | $where_params['dec_zero_date'] = '0004-12-31'; |
||
| 337 | } |
||
| 338 | } |
||
| 339 | // Special case - equals a date. |
||
| 340 | elseif ($param_info['type'] === 'date' && $search_params['types'][$param_name] === '=') |
||
| 341 | { |
||
| 342 | $query_parts[] = $param_info['db_fields'][0] . ' > ' . $search_params[$param_name] . ' AND ' . $param_info['db_fields'][0] . ' < ' . ($search_params[$param_name] + 86400); |
||
| 343 | } |
||
| 344 | else |
||
| 345 | { |
||
| 346 | $query_parts[] = $param_info['db_fields'][0] . ' ' . $range_trans[$search_params['types'][$param_name]] . ' ' . $search_params[$param_name]; |
||
| 347 | } |
||
| 348 | } |
||
| 349 | // Checkboxes. |
||
| 350 | elseif ($param_info['type'] === 'checkbox') |
||
| 351 | { |
||
| 352 | // Each checkbox or no checkbox at all is checked -> ignore. |
||
| 353 | if (!is_array($search_params[$param_name]) || $search_params[$param_name] === [] || count($search_params[$param_name]) === count($param_info['values'])) |
||
| 354 | { |
||
| 355 | continue; |
||
| 356 | } |
||
| 357 | |||
| 358 | $query_parts[] = ($param_info['db_fields'][0]) . ' IN ({array_string:' . $param_name . '_check})'; |
||
| 359 | $where_params[$param_name . '_check'] = $search_params[$param_name]; |
||
| 360 | } |
||
| 361 | else |
||
| 362 | { |
||
| 363 | // Replace the wildcard characters ('*' and '?') into MySQL ones. |
||
| 364 | $parameter = strtolower(strtr(Util::htmlspecialchars($search_params[$param_name], ENT_QUOTES), ['%' => '\%', '_' => '\_', '*' => '%', '?' => '_'])); |
||
| 365 | |||
| 366 | $query_parts[] = '({column_case_insensitive:' . implode('} LIKE {string_case_insensitive:' . $param_name . '_normal} OR {column_case_insensitive:', $param_info['db_fields']) . '} LIKE {string_case_insensitive:' . $param_name . '_normal})'; |
||
| 367 | |||
| 368 | $where_params[$param_name . '_normal'] = '%' . $parameter . '%'; |
||
| 369 | } |
||
| 370 | } |
||
| 371 | |||
| 372 | // Set up the membergroup query part. |
||
| 373 | $mg_query_parts = []; |
||
| 374 | |||
| 375 | // Primary membergroups, but only if at least was not selected. |
||
| 376 | if (!empty($search_params['membergroups'][1]) && count($context['membergroups']) !== count($search_params['membergroups'][1])) |
||
| 377 | { |
||
| 378 | $mg_query_parts[] = 'mem.id_group IN ({array_int:group_check})'; |
||
| 379 | $where_params['group_check'] = $search_params['membergroups'][1]; |
||
| 380 | } |
||
| 381 | |||
| 382 | // Additional membergroups (these are only relevant if not all primary groups where selected!). |
||
| 383 | if (!empty($search_params['membergroups'][2]) && (empty($search_params['membergroups'][1]) || count($context['membergroups']) !== count($search_params['membergroups'][1]))) |
||
| 384 | { |
||
| 385 | foreach ($search_params['membergroups'][2] as $mg) |
||
| 386 | { |
||
| 387 | $mg_query_parts[] = 'FIND_IN_SET({int:add_group_' . $mg . '}, mem.additional_groups) != 0'; |
||
| 388 | $where_params['add_group_' . $mg] = $mg; |
||
| 389 | } |
||
| 390 | } |
||
| 391 | |||
| 392 | // Combine the one or two membergroup parts into one query part linked with an OR. |
||
| 393 | if ($mg_query_parts !== []) |
||
| 394 | { |
||
| 395 | $query_parts[] = '(' . implode(' OR ', $mg_query_parts) . ')'; |
||
| 396 | } |
||
| 397 | |||
| 398 | // Get all selected post count related membergroups. |
||
| 399 | if (!empty($search_params['postgroups']) && count($search_params['postgroups']) !== count($context['postgroups'])) |
||
| 400 | { |
||
| 401 | $query_parts[] = 'id_post_group IN ({array_int:post_groups})'; |
||
| 402 | $where_params['post_groups'] = $search_params['postgroups']; |
||
| 403 | } |
||
| 404 | |||
| 405 | // Construct the where part of the query. |
||
| 406 | $where = $query_parts === [] ? '1=1' : implode(' |
||
| 407 | AND ', $query_parts); |
||
| 408 | } |
||
| 409 | else |
||
| 410 | { |
||
| 411 | $search_url_params = null; |
||
| 412 | } |
||
| 413 | |||
| 414 | // Construct the additional URL part with the query info in it. |
||
| 415 | $context['params_url'] = $context['sub_action'] === 'query' ? ['sa' => 'query', 'params' => $search_url_params] : []; |
||
| 416 | |||
| 417 | // Get the title and sub template ready.. |
||
| 418 | $context['page_title'] = $txt['admin_members']; |
||
| 419 | $where_params = $where_params ?? []; |
||
| 420 | |||
| 421 | $listOptions = [ |
||
| 422 | 'id' => 'member_list', |
||
| 423 | 'title' => $txt['members_list'], |
||
| 424 | 'items_per_page' => $modSettings['defaultMaxMembers'], |
||
| 425 | 'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'viewmembers'] + $context['params_url']), |
||
| 426 | 'default_sort_col' => 'user_name', |
||
| 427 | 'get_items' => [ |
||
| 428 | 'file' => SUBSDIR . '/Members.subs.php', |
||
| 429 | 'function' => 'list_getMembers', |
||
| 430 | 'params' => [ |
||
| 431 | $where ?? '1=1', |
||
| 432 | $where_params, |
||
| 433 | ], |
||
| 434 | ], |
||
| 435 | 'get_count' => [ |
||
| 436 | 'file' => SUBSDIR . '/Members.subs.php', |
||
| 437 | 'function' => 'list_getNumMembers', |
||
| 438 | 'params' => [ |
||
| 439 | $where ?? '1=1', |
||
| 440 | $where_params, |
||
| 441 | ], |
||
| 442 | ], |
||
| 443 | 'columns' => [ |
||
| 444 | 'id_member' => [ |
||
| 445 | 'header' => [ |
||
| 446 | 'value' => $txt['member_id'], |
||
| 447 | ], |
||
| 448 | 'data' => [ |
||
| 449 | 'db' => 'id_member', |
||
| 450 | ], |
||
| 451 | 'sort' => [ |
||
| 452 | 'default' => 'id_member', |
||
| 453 | 'reverse' => 'id_member DESC', |
||
| 454 | ], |
||
| 455 | ], |
||
| 456 | 'user_name' => [ |
||
| 457 | 'header' => [ |
||
| 458 | 'value' => $txt['username'], |
||
| 459 | ], |
||
| 460 | 'data' => [ |
||
| 461 | 'sprintf' => [ |
||
| 462 | 'format' => '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => '%1$d', 'name' => '%2$s']) . '">%2$s</a>', |
||
| 463 | 'params' => [ |
||
| 464 | 'id_member' => false, |
||
| 465 | 'member_name' => false, |
||
| 466 | ], |
||
| 467 | ], |
||
| 468 | ], |
||
| 469 | 'sort' => [ |
||
| 470 | 'default' => 'member_name', |
||
| 471 | 'reverse' => 'member_name DESC', |
||
| 472 | ], |
||
| 473 | ], |
||
| 474 | 'display_name' => [ |
||
| 475 | 'header' => [ |
||
| 476 | 'value' => $txt['display_name'], |
||
| 477 | ], |
||
| 478 | 'data' => [ |
||
| 479 | 'sprintf' => [ |
||
| 480 | 'format' => '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => '%1$d']) . '">%2$s</a>', |
||
| 481 | 'params' => [ |
||
| 482 | 'id_member' => false, |
||
| 483 | 'real_name' => false, |
||
| 484 | ], |
||
| 485 | ], |
||
| 486 | ], |
||
| 487 | 'sort' => [ |
||
| 488 | 'default' => 'real_name', |
||
| 489 | 'reverse' => 'real_name DESC', |
||
| 490 | ], |
||
| 491 | ], |
||
| 492 | 'email' => [ |
||
| 493 | 'header' => [ |
||
| 494 | 'value' => $txt['email_address'], |
||
| 495 | ], |
||
| 496 | 'data' => [ |
||
| 497 | 'sprintf' => [ |
||
| 498 | 'format' => '<a href="mailto:%1$s">%1$s</a>', |
||
| 499 | 'params' => [ |
||
| 500 | 'email_address' => true, |
||
| 501 | ], |
||
| 502 | ], |
||
| 503 | ], |
||
| 504 | 'sort' => [ |
||
| 505 | 'default' => 'email_address', |
||
| 506 | 'reverse' => 'email_address DESC', |
||
| 507 | ], |
||
| 508 | ], |
||
| 509 | 'ip' => [ |
||
| 510 | 'header' => [ |
||
| 511 | 'value' => $txt['ip_address'], |
||
| 512 | ], |
||
| 513 | 'data' => [ |
||
| 514 | 'sprintf' => [ |
||
| 515 | 'format' => '<a href="' . getUrl('action', ['action' => 'trackip', 'searchip' => '%1$s']) . '">%1$s</a>', |
||
| 516 | 'params' => [ |
||
| 517 | 'member_ip' => false, |
||
| 518 | ], |
||
| 519 | ], |
||
| 520 | ], |
||
| 521 | 'sort' => [ |
||
| 522 | 'default' => 'member_ip', |
||
| 523 | 'reverse' => 'member_ip DESC', |
||
| 524 | ], |
||
| 525 | ], |
||
| 526 | 'last_active' => [ |
||
| 527 | 'header' => [ |
||
| 528 | 'value' => $txt['viewmembers_online'], |
||
| 529 | ], |
||
| 530 | 'data' => [ |
||
| 531 | 'function' => static function ($rowData) { |
||
| 532 | global $txt; |
||
| 533 | |||
| 534 | require_once(SUBSDIR . '/Members.subs.php'); |
||
| 535 | |||
| 536 | // Calculate number of days since last online. |
||
| 537 | $difference = empty($rowData['last_login']) ? $txt['never'] : htmlTime($rowData['last_login']); |
||
| 538 | |||
| 539 | // Show it in italics if they're not activated... |
||
| 540 | if ($rowData['is_activated'] % 10 !== 1) |
||
| 541 | { |
||
| 542 | return sprintf('<em title="%1$s">%2$s</em>', $txt['not_activated'], $difference); |
||
| 543 | } |
||
| 544 | |||
| 545 | return $difference; |
||
| 546 | }, |
||
| 547 | ], |
||
| 548 | 'sort' => [ |
||
| 549 | 'default' => 'last_login DESC', |
||
| 550 | 'reverse' => 'last_login', |
||
| 551 | ], |
||
| 552 | ], |
||
| 553 | 'posts' => [ |
||
| 554 | 'header' => [ |
||
| 555 | 'value' => $txt['member_postcount'], |
||
| 556 | ], |
||
| 557 | 'data' => [ |
||
| 558 | 'db' => 'posts', |
||
| 559 | ], |
||
| 560 | 'sort' => [ |
||
| 561 | 'default' => 'posts', |
||
| 562 | 'reverse' => 'posts DESC', |
||
| 563 | ], |
||
| 564 | ], |
||
| 565 | 'check' => [ |
||
| 566 | 'header' => [ |
||
| 567 | 'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />', |
||
| 568 | 'class' => 'centertext', |
||
| 569 | ], |
||
| 570 | 'data' => [ |
||
| 571 | 'function' => static fn($rowData) => '<input type="checkbox" name="members[]" value="' . $rowData['id_member'] . '" class="input_check" ' . ($rowData['id_member'] === User::$info->id || $rowData['id_group'] == 1 || in_array(1, explode(',', $rowData['additional_groups'])) ? 'disabled="disabled"' : '') . ' />', |
||
|
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
| 572 | 'class' => 'centertext', |
||
| 573 | ], |
||
| 574 | ], |
||
| 575 | ], |
||
| 576 | 'form' => [ |
||
| 577 | 'href' => getUrl('admin', ['action' => 'admin', 'area' => 'viewmembers'] + $context['params_url']), |
||
| 578 | 'include_start' => true, |
||
| 579 | 'include_sort' => true, |
||
| 580 | ], |
||
| 581 | 'additional_rows' => [ |
||
| 582 | [ |
||
| 583 | 'position' => 'below_table_data', |
||
| 584 | 'value' => template_users_multiactions($this->_getGroups()), |
||
| 585 | 'class' => 'flow_flex_additional_row', |
||
| 586 | ], |
||
| 587 | ], |
||
| 588 | ]; |
||
| 589 | |||
| 590 | // Without enough permissions, don't show 'delete members' checkboxes. |
||
| 591 | if (!allowedTo('profile_remove_any')) |
||
| 592 | { |
||
| 593 | unset($listOptions['cols']['check'], $listOptions['form'], $listOptions['additional_rows']); |
||
| 594 | } |
||
| 595 | |||
| 596 | createList($listOptions); |
||
| 597 | |||
| 598 | $context['sub_template'] = 'show_list'; |
||
| 599 | $context['default_list'] = 'member_list'; |
||
| 600 | } |
||
| 601 | |||
| 602 | /** |
||
| 603 | * Handle mass action processing on a group of members |
||
| 604 | * |
||
| 605 | * - Deleting members |
||
| 606 | * - Group changes |
||
| 607 | * - Banning |
||
| 608 | */ |
||
| 609 | protected function _multiMembersAction(): void |
||
| 610 | { |
||
| 611 | global $txt; |
||
| 612 | |||
| 613 | // @todo add a token too? |
||
| 614 | checkSession(); |
||
| 615 | |||
| 616 | // Clean the input. |
||
| 617 | $members = []; |
||
| 618 | foreach ($this->_req->post->members as $value) |
||
| 619 | { |
||
| 620 | // Don't delete yourself, idiot. |
||
| 621 | if ($this->_req->post->maction === 'delete' && $value == $this->user->id) |
||
| 622 | { |
||
| 623 | continue; |
||
| 624 | } |
||
| 625 | |||
| 626 | $members[] = (int) $value; |
||
| 627 | } |
||
| 628 | |||
| 629 | $members = array_filter($members); |
||
| 630 | |||
| 631 | // No members, nothing to do. |
||
| 632 | if (empty($members)) |
||
| 633 | { |
||
| 634 | return; |
||
| 635 | } |
||
| 636 | |||
| 637 | // Are we performing a delete? |
||
| 638 | if ($this->_req->post->maction === 'delete' && allowedTo('profile_remove_any')) |
||
| 639 | { |
||
| 640 | // Delete all the selected members. |
||
| 641 | require_once(SUBSDIR . '/Members.subs.php'); |
||
| 642 | deleteMembers($members, true); |
||
| 643 | } |
||
| 644 | |||
| 645 | // Are we changing groups? |
||
| 646 | if (in_array($this->_req->post->maction, ['pgroup', 'agroup']) && allowedTo('manage_membergroups')) |
||
| 647 | { |
||
| 648 | require_once(SUBSDIR . '/Membergroups.subs.php'); |
||
| 649 | |||
| 650 | $groups = ['p', 'a']; |
||
| 651 | foreach ($groups as $group) |
||
| 652 | { |
||
| 653 | if ($this->_req->post->maction == $group . 'group' && !empty($this->_req->post->new_membergroup)) |
||
| 654 | { |
||
| 655 | $type = $group === 'p' ? 'force_primary' : 'only_additional'; |
||
| 656 | |||
| 657 | // Change all the selected members' group. |
||
| 658 | if ($this->_req->post->new_membergroup != -1) |
||
| 659 | { |
||
| 660 | addMembersToGroup($members, $this->_req->post->new_membergroup, $type, true); |
||
| 661 | } |
||
| 662 | else |
||
| 663 | { |
||
| 664 | removeMembersFromGroups($members, null, true); |
||
| 665 | } |
||
| 666 | } |
||
| 667 | } |
||
| 668 | } |
||
| 669 | |||
| 670 | // Are we banning? |
||
| 671 | if (in_array($this->_req->post->maction, ['ban_names', 'ban_mails', 'ban_ips', 'ban_names_mails']) && allowedTo('manage_bans')) |
||
| 672 | { |
||
| 673 | require_once(SUBSDIR . '/Bans.subs.php'); |
||
| 674 | require_once(SUBSDIR . '/Members.subs.php'); |
||
| 675 | |||
| 676 | $ban_group_id = insertBanGroup([ |
||
| 677 | 'name' => $txt['admin_ban_name'], |
||
| 678 | 'cannot' => [ |
||
| 679 | 'access' => 1, |
||
| 680 | 'register' => 0, |
||
| 681 | 'post' => 0, |
||
| 682 | 'login' => 0, |
||
| 683 | ], |
||
| 684 | 'db_expiration' => 'NULL', |
||
| 685 | 'reason' => '', |
||
| 686 | 'notes' => '', |
||
| 687 | ]); |
||
| 688 | |||
| 689 | $ban_name = in_array($this->_req->post->maction, ['ban_names', 'ban_names_mails']); |
||
| 690 | $ban_email = in_array($this->_req->post->maction, ['ban_mails', 'ban_names_mails']); |
||
| 691 | $ban_ips = $this->_req->post->maction === 'ban_ips'; |
||
| 692 | $suggestions = []; |
||
| 693 | |||
| 694 | if ($ban_email) |
||
| 695 | { |
||
| 696 | $suggestions[] = 'email'; |
||
| 697 | } |
||
| 698 | |||
| 699 | if ($ban_name) |
||
| 700 | { |
||
| 701 | $suggestions[] = 'user'; |
||
| 702 | } |
||
| 703 | |||
| 704 | if ($ban_ips) |
||
| 705 | { |
||
| 706 | $suggestions[] = 'main_ip'; |
||
| 707 | } |
||
| 708 | |||
| 709 | $members_data = getBasicMemberData($members, ['moderation' => true]); |
||
| 710 | foreach ($members_data as $member) |
||
| 711 | { |
||
| 712 | saveTriggers([ |
||
| 713 | 'main_ip' => $ban_ips ? $member['member_ip'] : '', |
||
| 714 | 'hostname' => '', |
||
| 715 | 'email' => $ban_email ? $member['email_address'] : '', |
||
| 716 | 'user' => $ban_name ? $member['member_name'] : '', |
||
| 717 | 'ban_suggestions' => $suggestions, |
||
| 718 | ], $ban_group_id, $ban_name ? $member['id_member'] : 0); |
||
| 719 | } |
||
| 720 | } |
||
| 721 | } |
||
| 722 | |||
| 723 | /** |
||
| 724 | * Prepares the list of groups to be used in the dropdown for "mass actions". |
||
| 725 | * |
||
| 726 | * @return array |
||
| 727 | */ |
||
| 728 | protected function _getGroups(): array |
||
| 729 | { |
||
| 730 | global $txt; |
||
| 731 | |||
| 732 | require_once(SUBSDIR . '/Membergroups.subs.php'); |
||
| 733 | |||
| 734 | $member_groups = getGroupsList(); |
||
| 735 | |||
| 736 | // Better remove admin membergroup...and set it to a "remove all" |
||
| 737 | $member_groups[1] = [ |
||
| 738 | 'id' => -1, |
||
| 739 | 'name' => $txt['remove_groups'], |
||
| 740 | 'is_primary' => 0, |
||
| 741 | ]; |
||
| 742 | |||
| 743 | // no primary is tricky... |
||
| 744 | $member_groups[0] = [ |
||
| 745 | 'id' => 0, |
||
| 746 | 'name' => '', |
||
| 747 | 'is_primary' => 1, |
||
| 748 | ]; |
||
| 749 | |||
| 750 | return $member_groups; |
||
| 751 | } |
||
| 752 | |||
| 753 | /** |
||
| 754 | * Search the member list, using one or more criteria. |
||
| 755 | * |
||
| 756 | * What it does: |
||
| 757 | * |
||
| 758 | * - Called by ?action=admin;area=viewmembers;sa=search. |
||
| 759 | * - Requires the moderate_forum permission. |
||
| 760 | * - form is submitted to action=admin;area=viewmembers;sa=query. |
||
| 761 | * |
||
| 762 | * @uses the search_members sub template of the ManageMembers template. |
||
| 763 | */ |
||
| 764 | public function action_search(): void |
||
| 765 | { |
||
| 766 | global $context, $txt; |
||
| 767 | |||
| 768 | // Get a list of all the membergroups and postgroups that can be selected. |
||
| 769 | require_once(SUBSDIR . '/Membergroups.subs.php'); |
||
| 770 | $groups = getBasicMembergroupData([], ['moderator'], null, true); |
||
| 771 | |||
| 772 | $context['membergroups'] = $groups['membergroups']; |
||
| 773 | $context['postgroups'] = $groups['postgroups']; |
||
| 774 | $context['page_title'] = $txt['admin_members']; |
||
| 775 | $context['sub_template'] = 'search_members'; |
||
| 776 | |||
| 777 | unset($groups); |
||
| 778 | } |
||
| 779 | |||
| 780 | /** |
||
| 781 | * List all members who are awaiting approval / activation, sortable on different columns. |
||
| 782 | * |
||
| 783 | * What it does: |
||
| 784 | * |
||
| 785 | * - It allows instant approval or activation of (a selection of) members. |
||
| 786 | * - Called by ?action=admin;area=viewmembers;sa=browse;type=approve |
||
| 787 | * or ?action=admin;area=viewmembers;sa=browse;type=activate. |
||
| 788 | * - The form submits to ?action=admin;area=viewmembers;sa=approve. |
||
| 789 | * - Requires the moderate_forum permission. |
||
| 790 | * |
||
| 791 | * @event integrate_list_approve_list |
||
| 792 | * @uses the admin_browse sub template of the ManageMembers template. |
||
| 793 | */ |
||
| 794 | public function action_browse(): void |
||
| 795 | { |
||
| 796 | global $txt, $context, $modSettings; |
||
| 797 | |||
| 798 | // Not a lot here! |
||
| 799 | $context['page_title'] = $txt['admin_members']; |
||
| 800 | $context['sub_template'] = 'admin_browse'; |
||
| 801 | $context['browse_type'] = $this->_req->query->type ?? (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1 ? 'activate' : 'approve'); |
||
| 802 | |||
| 803 | if (isset($context['tabs'][$context['browse_type']])) |
||
| 804 | { |
||
| 805 | $context['tabs'][$context['browse_type']]['is_selected'] = true; |
||
| 806 | } |
||
| 807 | |||
| 808 | // Allowed filters are those we can have, in theory. |
||
| 809 | $context['allowed_filters'] = $context['browse_type'] === 'approve' ? [3, 4, 5] : [0, 2]; |
||
| 810 | $context['current_filter'] = isset($this->_req->query->filter) && in_array($this->_req->query->filter, $context['allowed_filters']) && !empty($context['activation_numbers'][$this->_req->query->filter]) ? (int) $this->_req->query->filter : -1; |
||
| 811 | |||
| 812 | // Sort out the different sub areas that we can actually filter by. |
||
| 813 | $context['available_filters'] = []; |
||
| 814 | foreach ($context['activation_numbers'] as $type => $amount) |
||
| 815 | { |
||
| 816 | // We have some of these... |
||
| 817 | if (!in_array($type, $context['allowed_filters'])) |
||
| 818 | { |
||
| 819 | continue; |
||
| 820 | } |
||
| 821 | |||
| 822 | if ($amount <= 0) |
||
| 823 | { |
||
| 824 | continue; |
||
| 825 | } |
||
| 826 | |||
| 827 | $context['available_filters'][] = [ |
||
| 828 | 'type' => $type, |
||
| 829 | 'amount' => $amount, |
||
| 830 | 'desc' => $txt['admin_browse_filter_type_' . $type] ?? '?', |
||
| 831 | 'selected' => $type === $context['current_filter'] |
||
| 832 | ]; |
||
| 833 | } |
||
| 834 | |||
| 835 | // If the filter was not sent, set it to whatever has people in it! |
||
| 836 | if ($context['current_filter'] == -1 && !empty($context['available_filters'][0]['amount'])) |
||
| 837 | { |
||
| 838 | $context['current_filter'] = $context['available_filters'][0]['type']; |
||
| 839 | $context['available_filters'][0]['selected'] = true; |
||
| 840 | } |
||
| 841 | |||
| 842 | // This little variable is used to determine if we should flag where we are looking. |
||
| 843 | $context['show_filter'] = ($context['current_filter'] != 0 && $context['current_filter'] != 3) || count($context['available_filters']) > 1; |
||
| 844 | |||
| 845 | // The columns that can be sorted. |
||
| 846 | $context['columns'] = [ |
||
| 847 | 'id_member' => ['label' => $txt['admin_browse_id']], |
||
| 848 | 'member_name' => ['label' => $txt['admin_browse_username']], |
||
| 849 | 'email_address' => ['label' => $txt['admin_browse_email']], |
||
| 850 | 'member_ip' => ['label' => $txt['admin_browse_ip']], |
||
| 851 | 'date_registered' => ['label' => $txt['admin_browse_registered']], |
||
| 852 | ]; |
||
| 853 | |||
| 854 | // Are we showing duplicate information? |
||
| 855 | if (isset($this->_req->query->showdupes)) |
||
| 856 | { |
||
| 857 | $_SESSION['showdupes'] = (int) $this->_req->query->showdupes; |
||
| 858 | } |
||
| 859 | |||
| 860 | $context['show_duplicates'] = !empty($_SESSION['showdupes']); |
||
| 861 | |||
| 862 | // Determine which actions we should allow on this page. |
||
| 863 | $context['allowed_actions'] = []; |
||
| 864 | if ($context['browse_type'] === 'approve') |
||
| 865 | { |
||
| 866 | // If we are approving deleted accounts we have a slightly different list... actually a mirror ;) |
||
| 867 | if ($context['current_filter'] == 4) |
||
| 868 | { |
||
| 869 | $context['allowed_actions'] = [ |
||
| 870 | 'reject' => $txt['admin_browse_w_approve_deletion'], |
||
| 871 | 'ok' => $txt['admin_browse_w_reject_delete'], |
||
| 872 | ]; |
||
| 873 | } |
||
| 874 | else |
||
| 875 | { |
||
| 876 | $context['allowed_actions'] = [ |
||
| 877 | 'ok' => $txt['admin_browse_w_approve'], |
||
| 878 | 'okemail' => $txt['admin_browse_w_approve'] . ' ' . $txt['admin_browse_w_email'], |
||
| 879 | 'require_activation' => $txt['admin_browse_w_approve_require_activate'], |
||
| 880 | 'reject' => $txt['admin_browse_w_reject'], |
||
| 881 | 'rejectemail' => $txt['admin_browse_w_reject'] . ' ' . $txt['admin_browse_w_email'], |
||
| 882 | ]; |
||
| 883 | } |
||
| 884 | } |
||
| 885 | elseif ($context['browse_type'] === 'activate') |
||
| 886 | { |
||
| 887 | $context['allowed_actions'] = [ |
||
| 888 | 'ok' => $txt['admin_browse_w_activate'], |
||
| 889 | 'okemail' => $txt['admin_browse_w_activate'] . ' ' . $txt['admin_browse_w_email'], |
||
| 890 | 'delete' => $txt['admin_browse_w_delete'], |
||
| 891 | 'deleteemail' => $txt['admin_browse_w_delete'] . ' ' . $txt['admin_browse_w_email'], |
||
| 892 | 'remind' => $txt['admin_browse_w_remind'] . ' ' . $txt['admin_browse_w_email'], |
||
| 893 | ]; |
||
| 894 | } |
||
| 895 | |||
| 896 | // Create an option list for actions allowed to be done with selected members. |
||
| 897 | $allowed_actions = ' |
||
| 898 | <option selected="selected" value="">' . $txt['admin_browse_with_selected'] . ':</option> |
||
| 899 | <option value="" disabled="disabled">' . str_repeat('—', strlen($txt['admin_browse_with_selected'])) . '</option>'; |
||
| 900 | |||
| 901 | foreach ($context['allowed_actions'] as $key => $desc) |
||
| 902 | { |
||
| 903 | $allowed_actions .= ' |
||
| 904 | <option value="' . $key . '">' . '➤ ' . $desc . '</option>'; |
||
| 905 | } |
||
| 906 | |||
| 907 | // Setup the Javascript function for selecting an action for the list. |
||
| 908 | $javascript = ' |
||
| 909 | function onSelectChange() |
||
| 910 | { |
||
| 911 | if (document.forms.postForm.todo.value === "") |
||
| 912 | return; |
||
| 913 | |||
| 914 | var message = "";'; |
||
| 915 | |||
| 916 | // We have special messages for approving deletion of accounts - it's surprisingly logical - honest. |
||
| 917 | if ($context['current_filter'] == 4) |
||
| 918 | { |
||
| 919 | $javascript .= ' |
||
| 920 | if (document.forms.postForm.todo.value.indexOf("reject") !== -1) |
||
| 921 | message = "' . $txt['admin_browse_w_delete'] . '"; |
||
| 922 | else |
||
| 923 | message = "' . $txt['admin_browse_w_reject'] . '";'; |
||
| 924 | } |
||
| 925 | // Otherwise a nice standard message. |
||
| 926 | else |
||
| 927 | { |
||
| 928 | $javascript .= ' |
||
| 929 | if (document.forms.postForm.todo.value.indexOf("delete") !== -1) |
||
| 930 | message = "' . $txt['admin_browse_w_delete'] . '"; |
||
| 931 | else if (document.forms.postForm.todo.value.indexOf("reject") !== -1) |
||
| 932 | message = "' . $txt['admin_browse_w_reject'] . '"; |
||
| 933 | else if (document.forms.postForm.todo.value == "remind") |
||
| 934 | message = "' . $txt['admin_browse_w_remind'] . '"; |
||
| 935 | else |
||
| 936 | message = "' . ($context['browse_type'] === 'approve' ? $txt['admin_browse_w_approve'] : $txt['admin_browse_w_activate']) . '";'; |
||
| 937 | } |
||
| 938 | |||
| 939 | $javascript .= ' |
||
| 940 | if (confirm(message + " ' . $txt['admin_browse_warn'] . '")) |
||
| 941 | document.forms.postForm.submit(); |
||
| 942 | }'; |
||
| 943 | |||
| 944 | $listOptions = [ |
||
| 945 | 'id' => 'approve_list', |
||
| 946 | 'items_per_page' => $modSettings['defaultMaxMembers'], |
||
| 947 | 'base_href' => getUrl('admin', ['action' => 'admin', 'area' => 'viewmembers', 'sa' => 'browse', 'type' => $context['browse_type'] . (empty($context['show_filter']) ? '' : ", 'filter' =>" . $context['current_filter'])]), |
||
| 948 | 'default_sort_col' => 'date_registered', |
||
| 949 | 'get_items' => [ |
||
| 950 | 'file' => SUBSDIR . '/Members.subs.php', |
||
| 951 | 'function' => 'list_getMembers', |
||
| 952 | 'params' => [ |
||
| 953 | 'is_activated = {int:activated_status}', |
||
| 954 | ['activated_status' => $context['current_filter']], |
||
| 955 | $context['show_duplicates'], |
||
| 956 | ], |
||
| 957 | ], |
||
| 958 | 'get_count' => [ |
||
| 959 | 'file' => SUBSDIR . '/Members.subs.php', |
||
| 960 | 'function' => 'list_getNumMembers', |
||
| 961 | 'params' => [ |
||
| 962 | 'is_activated = {int:activated_status}', |
||
| 963 | ['activated_status' => $context['current_filter']], |
||
| 964 | ], |
||
| 965 | ], |
||
| 966 | 'columns' => [ |
||
| 967 | 'id_member' => [ |
||
| 968 | 'header' => [ |
||
| 969 | 'value' => $txt['member_id'], |
||
| 970 | ], |
||
| 971 | 'data' => [ |
||
| 972 | 'db' => 'id_member', |
||
| 973 | ], |
||
| 974 | 'sort' => [ |
||
| 975 | 'default' => 'id_member', |
||
| 976 | 'reverse' => 'id_member DESC', |
||
| 977 | ], |
||
| 978 | ], |
||
| 979 | 'user_name' => [ |
||
| 980 | 'header' => [ |
||
| 981 | 'value' => $txt['username'], |
||
| 982 | ], |
||
| 983 | 'data' => [ |
||
| 984 | 'sprintf' => [ |
||
| 985 | 'format' => '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => '%1$d']) . '">%2$s</a>', |
||
| 986 | 'params' => [ |
||
| 987 | 'id_member' => false, |
||
| 988 | 'member_name' => false, |
||
| 989 | ], |
||
| 990 | ], |
||
| 991 | ], |
||
| 992 | 'sort' => [ |
||
| 993 | 'default' => 'member_name', |
||
| 994 | 'reverse' => 'member_name DESC', |
||
| 995 | ], |
||
| 996 | ], |
||
| 997 | 'email' => [ |
||
| 998 | 'header' => [ |
||
| 999 | 'value' => $txt['email_address'], |
||
| 1000 | ], |
||
| 1001 | 'data' => [ |
||
| 1002 | 'sprintf' => [ |
||
| 1003 | 'format' => '<a href="mailto:%1$s">%1$s</a>', |
||
| 1004 | 'params' => [ |
||
| 1005 | 'email_address' => true, |
||
| 1006 | ], |
||
| 1007 | ], |
||
| 1008 | ], |
||
| 1009 | 'sort' => [ |
||
| 1010 | 'default' => 'email_address', |
||
| 1011 | 'reverse' => 'email_address DESC', |
||
| 1012 | ], |
||
| 1013 | ], |
||
| 1014 | 'ip' => [ |
||
| 1015 | 'header' => [ |
||
| 1016 | 'value' => $txt['ip_address'], |
||
| 1017 | ], |
||
| 1018 | 'data' => [ |
||
| 1019 | 'sprintf' => [ |
||
| 1020 | 'format' => '<a href="' . getUrl('profile', ['action' => 'trackip', 'searchip' => '%1$s']) . '">%1$s</a>', |
||
| 1021 | 'params' => [ |
||
| 1022 | 'member_ip' => false, |
||
| 1023 | ], |
||
| 1024 | ], |
||
| 1025 | ], |
||
| 1026 | 'sort' => [ |
||
| 1027 | 'default' => 'member_ip', |
||
| 1028 | 'reverse' => 'member_ip DESC', |
||
| 1029 | ], |
||
| 1030 | ], |
||
| 1031 | 'hostname' => [ |
||
| 1032 | 'header' => [ |
||
| 1033 | 'value' => $txt['hostname'], |
||
| 1034 | ], |
||
| 1035 | 'data' => [ |
||
| 1036 | 'function' => static fn($rowData) => host_from_ip($rowData['member_ip']), |
||
| 1037 | 'class' => 'smalltext', |
||
| 1038 | ], |
||
| 1039 | ], |
||
| 1040 | 'date_registered' => [ |
||
| 1041 | 'header' => [ |
||
| 1042 | 'value' => $context['current_filter'] == 4 ? $txt['viewmembers_online'] : $txt['date_registered'], |
||
| 1043 | ], |
||
| 1044 | 'data' => [ |
||
| 1045 | 'function' => static function ($rowData) { |
||
| 1046 | global $context; |
||
| 1047 | return standardTime($rowData[($context['current_filter'] == 4 ? 'last_login' : 'date_registered')]); |
||
| 1048 | }, |
||
| 1049 | ], |
||
| 1050 | 'sort' => [ |
||
| 1051 | 'default' => $context['current_filter'] == 4 ? 'mem.last_login DESC' : 'date_registered DESC', |
||
| 1052 | 'reverse' => $context['current_filter'] == 4 ? 'mem.last_login' : 'date_registered', |
||
| 1053 | ], |
||
| 1054 | ], |
||
| 1055 | 'duplicates' => [ |
||
| 1056 | 'header' => [ |
||
| 1057 | 'value' => $txt['duplicates'], |
||
| 1058 | // Make sure it doesn't go too wide. |
||
| 1059 | 'style' => 'width: 20%;', |
||
| 1060 | ], |
||
| 1061 | 'data' => [ |
||
| 1062 | 'function' => static function ($rowData) { |
||
| 1063 | global $txt; |
||
| 1064 | |||
| 1065 | $member_links = []; |
||
| 1066 | foreach ($rowData['duplicate_members'] as $member) |
||
| 1067 | { |
||
| 1068 | if ($member['id']) |
||
| 1069 | { |
||
| 1070 | $member_links[] = '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $member['id'], 'name' => $member['name']]) . '" ' . (empty($member['is_banned']) ? '' : 'class="alert"') . '>' . $member['name'] . '</a>'; |
||
| 1071 | } |
||
| 1072 | else |
||
| 1073 | { |
||
| 1074 | $member_links[] = $member['name'] . ' (' . $txt['guest'] . ')'; |
||
| 1075 | } |
||
| 1076 | } |
||
| 1077 | |||
| 1078 | return implode(', ', $member_links); |
||
| 1079 | }, |
||
| 1080 | 'class' => 'smalltext', |
||
| 1081 | ], |
||
| 1082 | ], |
||
| 1083 | 'check' => [ |
||
| 1084 | 'header' => [ |
||
| 1085 | 'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />', |
||
| 1086 | 'class' => 'centertext', |
||
| 1087 | ], |
||
| 1088 | 'data' => [ |
||
| 1089 | 'sprintf' => [ |
||
| 1090 | 'format' => '<input type="checkbox" name="todoAction[]" value="%1$d" class="input_check" />', |
||
| 1091 | 'params' => [ |
||
| 1092 | 'id_member' => false, |
||
| 1093 | ], |
||
| 1094 | ], |
||
| 1095 | 'class' => 'centertext', |
||
| 1096 | ], |
||
| 1097 | ], |
||
| 1098 | ], |
||
| 1099 | 'javascript' => $javascript, |
||
| 1100 | 'form' => [ |
||
| 1101 | 'href' => getUrl('action', ['action' => 'admin', 'area' => 'viewmembers', 'sa' => 'approve', 'type' => $context['browse_type']]), |
||
| 1102 | 'name' => 'postForm', |
||
| 1103 | 'include_start' => true, |
||
| 1104 | 'include_sort' => true, |
||
| 1105 | 'hidden_fields' => [ |
||
| 1106 | 'orig_filter' => $context['current_filter'], |
||
| 1107 | ], |
||
| 1108 | ], |
||
| 1109 | 'additional_rows' => [ |
||
| 1110 | [ |
||
| 1111 | 'position' => 'below_table_data', |
||
| 1112 | 'class' => 'flow_flex_additional_row', |
||
| 1113 | 'value' => ' |
||
| 1114 | <div class="submitbutton"> |
||
| 1115 | <a class="linkbutton" href="' . getUrl('action', ['action' => 'admin', 'area' => 'viewmembers', 'sa' => 'browse', 'showdupes' => $context['show_duplicates'] ? 0 : 1, 'type' => $context['browse_type'], '{session_data}'] + (empty($context['show_filter']) ? [] : ['filter' => $context['current_filter']])) . '">' . ($context['show_duplicates'] ? $txt['dont_check_for_duplicate'] : $txt['check_for_duplicate']) . '</a> |
||
| 1116 | <select name="todo" onchange="onSelectChange();"> |
||
| 1117 | ' . $allowed_actions . ' |
||
| 1118 | </select> |
||
| 1119 | <noscript> |
||
| 1120 | <input type="submit" value="' . $txt['go'] . '" /> |
||
| 1121 | </noscript> |
||
| 1122 | </div> |
||
| 1123 | ', |
||
| 1124 | ], |
||
| 1125 | ], |
||
| 1126 | ]; |
||
| 1127 | |||
| 1128 | // Pick what column to actually include if we're showing duplicates. |
||
| 1129 | if ($context['show_duplicates']) |
||
| 1130 | { |
||
| 1131 | unset($listOptions['columns']['email']); |
||
| 1132 | } |
||
| 1133 | else |
||
| 1134 | { |
||
| 1135 | unset($listOptions['columns']['duplicates']); |
||
| 1136 | } |
||
| 1137 | |||
| 1138 | // Only show hostname on duplicates as it takes a lot of time. |
||
| 1139 | if (!$context['show_duplicates'] || !empty($modSettings['disableHostnameLookup'])) |
||
| 1140 | { |
||
| 1141 | unset($listOptions['columns']['hostname']); |
||
| 1142 | } |
||
| 1143 | |||
| 1144 | // Is there any need to show filters? |
||
| 1145 | if (isset($context['available_filters'])) |
||
| 1146 | { |
||
| 1147 | $listOptions['list_menu'] = [ |
||
| 1148 | 'show_on' => 'top', |
||
| 1149 | 'links' => [] |
||
| 1150 | ]; |
||
| 1151 | |||
| 1152 | foreach ($context['available_filters'] as $filter) |
||
| 1153 | { |
||
| 1154 | $listOptions['list_menu']['links'][] = [ |
||
| 1155 | 'is_selected' => $filter['selected'], |
||
| 1156 | 'href' => getUrl('action', ['action' => 'admin', 'area' => 'viewmembers', 'sa' => 'browse', 'type' => $context['browse_type'], 'filter' => $filter['type']]), |
||
| 1157 | 'label' => $filter['desc'] . ' - ' . $filter['amount'] . ' ' . ($filter['amount'] == 1 ? $txt['user'] : $txt['users']) |
||
| 1158 | ]; |
||
| 1159 | } |
||
| 1160 | } |
||
| 1161 | |||
| 1162 | // Now that we have all the options, create the list. |
||
| 1163 | createList($listOptions); |
||
| 1164 | } |
||
| 1165 | |||
| 1166 | /** |
||
| 1167 | * This function handles the approval, rejection, activation or deletion of members. |
||
| 1168 | * |
||
| 1169 | * What it does: |
||
| 1170 | * |
||
| 1171 | * - Called by ?action=admin;area=viewmembers;sa=approve. |
||
| 1172 | * - Requires the moderate_forum permission. |
||
| 1173 | * - Redirects to ?action=admin;area=viewmembers;sa=browse |
||
| 1174 | * with the same parameters as the calling page. |
||
| 1175 | */ |
||
| 1176 | public function action_approve(): void |
||
| 1177 | { |
||
| 1178 | global $modSettings; |
||
| 1179 | |||
| 1180 | // First, check our session. |
||
| 1181 | checkSession(); |
||
| 1182 | |||
| 1183 | require_once(SUBSDIR . '/Mail.subs.php'); |
||
| 1184 | require_once(SUBSDIR . '/Members.subs.php'); |
||
| 1185 | |||
| 1186 | // We also need to the login languages here - for emails. |
||
| 1187 | Txt::load('Login'); |
||
| 1188 | |||
| 1189 | // Start off clean |
||
| 1190 | $this->conditions = []; |
||
| 1191 | |||
| 1192 | // Sort out where we are going... |
||
| 1193 | $original_filter = $this->_req->getPost('orig_filter', 'intval', null); |
||
| 1194 | $current_filter = $this->conditions['activated_status'] = $original_filter; |
||
| 1195 | |||
| 1196 | $type = $this->_req->getQuery('type', 'trim', ''); |
||
| 1197 | $filter = $this->_req->getPost('filter', 'trim', null); |
||
| 1198 | $sort = $this->_req->getRequest('sort', 'trim', ''); |
||
| 1199 | $start = $this->_req->getRequest('start', 'intval', 0); |
||
| 1200 | $todoAction = $this->_req->getPost('todoAction'); |
||
| 1201 | $time_passed = $this->_req->getPost('$time_passed', 'intval'); |
||
| 1202 | $todo = $this->_req->getPost('todo', 'trim'); |
||
| 1203 | |||
| 1204 | // If we are applying a filter do just that - then redirect. |
||
| 1205 | if (isset($filter) && $filter !== $original_filter) |
||
| 1206 | { |
||
| 1207 | redirectexit('action=admin;area=viewmembers;sa=browse;type=' . $type . ';sort=' . $sort . ';filter=' . $filter . ';start=' . $start); |
||
| 1208 | } |
||
| 1209 | |||
| 1210 | // Nothing to do? |
||
| 1211 | if (!isset($todoAction) && !isset($time_passed)) |
||
| 1212 | { |
||
| 1213 | redirectexit('action=admin;area=viewmembers;sa=browse;type=' . $type . ';sort=' . $sort . ';filter=' . $current_filter . ';start=' . $start); |
||
| 1214 | } |
||
| 1215 | |||
| 1216 | // Are we dealing with members who have been waiting for > set amount of time? |
||
| 1217 | if (isset($time_passed)) |
||
| 1218 | { |
||
| 1219 | $this->conditions['time_before'] = time() - 86400 * $time_passed; |
||
| 1220 | } |
||
| 1221 | // Coming from checkboxes - validate the members passed through to us. |
||
| 1222 | else |
||
| 1223 | { |
||
| 1224 | $this->conditions['members'] = []; |
||
| 1225 | foreach ($todoAction as $id) |
||
| 1226 | { |
||
| 1227 | $this->conditions['members'][] = (int) $id; |
||
| 1228 | } |
||
| 1229 | } |
||
| 1230 | |||
| 1231 | $data = retrieveMemberData($this->conditions); |
||
| 1232 | if ($data['member_count'] === 0) |
||
| 1233 | { |
||
| 1234 | redirectexit('action=admin;area=viewmembers;sa=browse;type=' . $type . ';sort=' . $sort . ';filter=' . $current_filter . ';start=' . $start); |
||
| 1235 | } |
||
| 1236 | |||
| 1237 | $this->member_info = $data['member_info']; |
||
| 1238 | $this->conditions['members'] = $data['members']; |
||
| 1239 | |||
| 1240 | // What do we want to do with this application? |
||
| 1241 | switch ($todo) |
||
| 1242 | { |
||
| 1243 | // Are we activating or approving the members? |
||
| 1244 | case 'ok': |
||
| 1245 | case 'okemail': |
||
| 1246 | $this->_okMember(); |
||
| 1247 | break; |
||
| 1248 | // Maybe we're sending it off for activation? |
||
| 1249 | case 'require_activation': |
||
| 1250 | $this->_requireMember(); |
||
| 1251 | break; |
||
| 1252 | // Are we rejecting them? |
||
| 1253 | case 'reject': |
||
| 1254 | case 'rejectemail': |
||
| 1255 | $this->_rejectMember(); |
||
| 1256 | break; |
||
| 1257 | // A simple delete? |
||
| 1258 | case 'delete': |
||
| 1259 | case 'deleteemail': |
||
| 1260 | $this->_deleteMember(); |
||
| 1261 | break; |
||
| 1262 | // Remind them to activate their account? |
||
| 1263 | case 'remind': |
||
| 1264 | $this->_remindMember(); |
||
| 1265 | break; |
||
| 1266 | } |
||
| 1267 | |||
| 1268 | // Log what we did? Core features Moderation Logging must be enabled |
||
| 1269 | if (featureEnabled('ml') && in_array($todo, ['ok', 'okemail', 'require_activation', 'remind'])) |
||
| 1270 | { |
||
| 1271 | $log_action = $todo === 'remind' ? 'remind_member' : 'approve_member'; |
||
| 1272 | |||
| 1273 | foreach ($this->member_info as $member) |
||
| 1274 | { |
||
| 1275 | logAction($log_action, ['member' => $member['id']], 'admin'); |
||
| 1276 | } |
||
| 1277 | } |
||
| 1278 | |||
| 1279 | // Although updateMemberStats *may* catch this, best to do it manually just in case (Doesn't always sort out unapprovedMembers). |
||
| 1280 | if (in_array($current_filter, [3, 4, 5])) |
||
| 1281 | { |
||
| 1282 | updateSettings(['unapprovedMembers' => ($modSettings['unapprovedMembers'] > $data['member_count'] ? $modSettings['unapprovedMembers'] - $data['member_count'] : 0)]); |
||
| 1283 | } |
||
| 1284 | |||
| 1285 | // Update the member's stats. (but, we know the member didn't change their name.) |
||
| 1286 | require_once(SUBSDIR . '/Members.subs.php'); |
||
| 1287 | updateMemberStats(); |
||
| 1288 | |||
| 1289 | // If they haven't been deleted, update the post group statistics on them... |
||
| 1290 | if (!in_array($todo, ['delete', 'deleteemail', 'reject', 'rejectemail', 'remind'])) |
||
| 1291 | { |
||
| 1292 | require_once(SUBSDIR . '/Membergroups.subs.php'); |
||
| 1293 | updatePostGroupStats($this->conditions['members']); |
||
| 1294 | } |
||
| 1295 | |||
| 1296 | redirectexit('action=admin;area=viewmembers;sa=browse;type=' . $type . ';sort=' . $sort . ';filter=' . $current_filter . ';start=' . $start); |
||
| 1297 | } |
||
| 1298 | |||
| 1299 | /** |
||
| 1300 | * Approve a member application |
||
| 1301 | */ |
||
| 1302 | private function _okMember(): void |
||
| 1303 | { |
||
| 1304 | // Approve / activate this member. |
||
| 1305 | approveMembers($this->conditions); |
||
| 1306 | |||
| 1307 | // Check for email. |
||
| 1308 | if ($this->_req->post->todo === 'okemail') |
||
| 1309 | { |
||
| 1310 | foreach ($this->member_info as $member) |
||
| 1311 | { |
||
| 1312 | $replacements = [ |
||
| 1313 | 'NAME' => $member['name'], |
||
| 1314 | 'USERNAME' => $member['username'], |
||
| 1315 | 'PROFILELINK' => getUrl('profile', ['action' => 'profile', 'u' => $member['id'], 'name' => $member['name']]), |
||
| 1316 | 'FORGOTPASSWORDLINK' => getUrl('action', ['action' => 'reminder']), |
||
| 1317 | ]; |
||
| 1318 | |||
| 1319 | $emaildata = loadEmailTemplate('admin_approve_accept', $replacements, $member['language']); |
||
| 1320 | sendmail($member['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 0); |
||
| 1321 | } |
||
| 1322 | } |
||
| 1323 | |||
| 1324 | // Update the menu action cache so its forced to refresh |
||
| 1325 | Cache::instance()->remove('num_menu_errors'); |
||
| 1326 | } |
||
| 1327 | |||
| 1328 | /** |
||
| 1329 | * Tell some members that they require activation of their account |
||
| 1330 | */ |
||
| 1331 | private function _requireMember(): void |
||
| 1332 | { |
||
| 1333 | require_once(SUBSDIR . '/Auth.subs.php'); |
||
| 1334 | |||
| 1335 | // We have to do this for each member I'm afraid. |
||
| 1336 | foreach ($this->member_info as $member) |
||
| 1337 | { |
||
| 1338 | $this->conditions['selected_member'] = $member['id']; |
||
| 1339 | |||
| 1340 | // Generate a random activation code. |
||
| 1341 | $this->conditions['validation_code'] = generateValidationCode(14); |
||
| 1342 | |||
| 1343 | // Set these members for activation - I know this includes two id_member checks but it's safer than bodging $condition ;). |
||
| 1344 | enforceReactivation($this->conditions); |
||
| 1345 | |||
| 1346 | $replacements = [ |
||
| 1347 | 'USERNAME' => $member['name'], |
||
| 1348 | 'ACTIVATIONLINK' => getUrl('action', ['action' => 'register', 'sa' => 'activate', 'u' => $member['id'], 'code' => $this->conditions['validation_code']]), |
||
| 1349 | 'ACTIVATIONLINKWITHOUTCODE' => getUrl('action', ['action' => 'register', 'sa' => 'activate', 'u' => $member['id']]), |
||
| 1350 | 'ACTIVATIONCODE' => $this->conditions['validation_code'], |
||
| 1351 | ]; |
||
| 1352 | |||
| 1353 | $emaildata = loadEmailTemplate('admin_approve_activation', $replacements, $member['language']); |
||
| 1354 | sendmail($member['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 0); |
||
| 1355 | } |
||
| 1356 | } |
||
| 1357 | |||
| 1358 | /** |
||
| 1359 | * Reject a set a member applications, maybe even tell them |
||
| 1360 | */ |
||
| 1361 | private function _rejectMember(): void |
||
| 1362 | { |
||
| 1363 | deleteMembers($this->conditions['members']); |
||
| 1364 | |||
| 1365 | // Send email telling them they aren't welcome? |
||
| 1366 | if ($this->_req->post->todo === 'rejectemail') |
||
| 1367 | { |
||
| 1368 | foreach ($this->member_info as $member) |
||
| 1369 | { |
||
| 1370 | $replacements = [ |
||
| 1371 | 'USERNAME' => $member['name'], |
||
| 1372 | ]; |
||
| 1373 | |||
| 1374 | $emaildata = loadEmailTemplate('admin_approve_reject', $replacements, $member['language']); |
||
| 1375 | sendmail($member['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 1); |
||
| 1376 | } |
||
| 1377 | } |
||
| 1378 | } |
||
| 1379 | |||
| 1380 | /** |
||
| 1381 | * Deletes the members specified in the conditions array. |
||
| 1382 | * |
||
| 1383 | * What it does: |
||
| 1384 | * |
||
| 1385 | * - Called by the action_approve method. |
||
| 1386 | * - Deletes the members specified in the conditions array. |
||
| 1387 | * - Optionally sends email to notify the deleted members. |
||
| 1388 | * |
||
| 1389 | * @return void |
||
| 1390 | */ |
||
| 1391 | private function _deleteMember(): void |
||
| 1392 | { |
||
| 1393 | deleteMembers($this->conditions['members']); |
||
| 1394 | |||
| 1395 | // Send email telling them they aren't welcome? |
||
| 1396 | if ($this->_req->post->todo === 'deleteemail') |
||
| 1397 | { |
||
| 1398 | foreach ($this->member_info as $member) |
||
| 1399 | { |
||
| 1400 | $replacements = [ |
||
| 1401 | 'USERNAME' => $member['name'], |
||
| 1402 | ]; |
||
| 1403 | |||
| 1404 | $emaildata = loadEmailTemplate('admin_approve_delete', $replacements, $member['language']); |
||
| 1405 | sendmail($member['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 1); |
||
| 1406 | } |
||
| 1407 | } |
||
| 1408 | } |
||
| 1409 | |||
| 1410 | /** |
||
| 1411 | * Remind a set of members that they have an activation email waiting |
||
| 1412 | */ |
||
| 1413 | private function _remindMember(): void |
||
| 1414 | { |
||
| 1415 | require_once(SUBSDIR . '/Auth.subs.php'); |
||
| 1416 | |||
| 1417 | foreach ($this->member_info as $member) |
||
| 1418 | { |
||
| 1419 | $this->conditions['selected_member'] = $member['id']; |
||
| 1420 | $this->conditions['validation_code'] = generateValidationCode(14); |
||
| 1421 | |||
| 1422 | enforceReactivation($this->conditions); |
||
| 1423 | |||
| 1424 | $replacements = [ |
||
| 1425 | 'USERNAME' => $member['name'], |
||
| 1426 | 'ACTIVATIONLINK' => getUrl('action', ['action' => 'register', 'sa' => 'activate', 'u' => $member['id'], 'code' => $this->conditions['validation_code']]), |
||
| 1427 | 'ACTIVATIONLINKWITHOUTCODE' => getUrl('action', ['action' => 'register', 'sa' => 'activate', 'u' => $member['id']]), |
||
| 1428 | 'ACTIVATIONCODE' => $this->conditions['validation_code'], |
||
| 1429 | ]; |
||
| 1430 | |||
| 1431 | $emaildata = loadEmailTemplate('admin_approve_remind', $replacements, $member['language']); |
||
| 1432 | sendmail($member['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 1); |
||
| 1433 | } |
||
| 1434 | } |
||
| 1435 | } |
||
| 1436 |