PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.0
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.0
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
← All changes | back/Classes/UserClass.php +450 -244 10.2.2 → 11.1.0 View file →
@@ -8,26 +8,44 @@
8 8 use AcyMailing\Core\AcymClass;
9 9
10 10 class UserClass extends AcymClass
11 11 {
12 - var $checkVisitor = true;
13 - var $restrictedFields = ['id', 'key', 'confirmed', 'active', 'cms_id', 'creation_date'];
14 - var $allowModif = false;
15 - var $requireId = false;
16 - var $sendConf = true;
17 - var $forceConf = false;
12 + public const STATUS_SUBSCRIBED = 1;
13 + private const RESTRICTED_FIELDS = [
14 + 'id',
15 + 'key',
16 + 'confirmed',
17 + 'active',
18 + 'cms_id',
19 + 'creation_date',
20 + ];
21 +
22 + private const FORM_ALLOWED_FIELDS = [
23 + 'email',
24 + 'name',
25 + 'language',
26 + 'tracking',
27 + ];
28 +
29 + public bool $checkVisitor = true;
30 + public bool $requireId = false;
31 + public bool $sendConf = true;
18 32 public bool $confirmationSentSuccess = false;
19 - var $newUser = false;
20 - var $blockNotifications = false;
21 - var $subscribed = false;
22 - var $confirmationSentError;
23 - var $triggers = true;
33 + public bool $newUser = false;
34 + public bool $blockNotifications = false;
35 + public bool $subscribed = false;
36 + public string $confirmationSentError;
37 + public bool $triggers = true;
24 38
25 - var $forceConfAdmin = false;
39 + // For integration, for example someone is activating user subscription on membership pro on the joomla backend and he needs to send the confirmation emails
40 + public bool $forceConfAdmin = false;
26 41
27 - public $sendWelcomeEmail = true;
28 - public $sendUnsubscribeEmail = true;
42 + public bool $sendWelcomeEmail = true;
43 + public bool $sendUnsubscribeEmail = true;
29 44
45 + private bool $allowModif = false;
46 + private bool $forceConf = false;
47 +
30 48 public function __construct()
31 49 {
32 50 parent::__construct();
33 51
@@ -34,15 +52,19 @@
34 52 $this->table = 'user';
35 53 $this->pkey = 'id';
36 54 }
37 55
56 + /**
57 + * Get users depending on filters (search, status, pagination)
58 + */
38 59 public function getMatchingElements(array $settings = []): array
39 60 {
40 61 $results = [];
62 + // Initialize the queries
41 63 $columns = '`user`.*';
42 64 if (!empty($settings['columns'])) {
43 65 foreach ($settings['columns'] as $key => $value) {
44 - $settings['columns'][$key] = $key === 'join' ? $value : 'user.'.$value;
66 + $settings['columns'][$key] = $key === 'join' ? acym_secureDBColumn($value) : 'user.'.acym_secureDBColumn($value);
45 67 }
46 68 $columns = implode(', ', $settings['columns']);
47 69 }
48 70
@@ -56,13 +78,15 @@
56 78 }
57 79 $this->handleSegmentFilter($settings, $filters, $query, $queryCount, $queryStatus);
58 80 $this->handleSubscriptionFilter($settings, $filters, $query, $queryCount, $queryStatus);
59 81 $this->handleFrontend($settings, $query, $queryCount, $queryStatus);
60 - $this->handleSearchFilter($settings, $query, $queryCount, $queryStatus, $filters);
61 - $results['status'] = $this->handleUserStatusFilter($settings, $queryStatus, $filters);
82 + $this->handleSearchFilter($settings, $filters, $query, $queryCount, $queryStatus);
83 + $results['status'] = $this->handleUserStatusFilter($settings, $filters, $queryStatus);
62 84 $entityResult = $this->handleEntitySelect($settings, $filters);
63 85
64 - if (!empty($entityResult)) return $entityResult;
86 + if (!empty($entityResult)) {
87 + return $entityResult;
88 + }
65 89
66 90 if (!empty($filters)) {
67 91 $query .= ' WHERE ('.implode(') AND (', $filters).')';
68 92 $queryCount .= ' WHERE ('.implode(') AND (', $filters).')';
@@ -80,12 +104,15 @@
80 104 $pagination = new PaginationHelper();
81 105 $settings['elementsPerPage'] = $pagination->getListLimit();
82 106 }
83 107
108 + // This line warns the database that a big query is about to be ran
84 109 acym_query('SET SQL_BIG_SELECTS=1');
110 + // The beginning of $query can be modified in handleSearchFilter, don't move the SELECT up
85 111 $results['elements'] = acym_loadObjectList('SELECT DISTINCT '.$query, '', $settings['offset'], $settings['elementsPerPage']);
86 112 $results['total'] = acym_loadObject($queryCount);
87 113
114 + // Get CMS username separately if needed
88 115 if (!empty($results['elements']) && !empty($settings['cms_username'])) {
89 116 $cmsIds = array_diff(array_column($results['elements'], 'cms_id'), [0, '']);
90 117 if (!empty($cmsIds)) {
91 118 global $acymCmsUserVars;
@@ -96,8 +123,9 @@
96 123 'id'
97 124 );
98 125 foreach ($results['elements'] as $key => $oneElement) {
99 126 if (empty($results['elements'][$key]->cms_id)) continue;
127 + // Clean cms link on subscribers where CMS user no longer exists
100 128 if (empty($userNames[$results['elements'][$key]->cms_id])) {
101 129 $oneUser = $this->getOneById($results['elements'][$key]->id);
102 130 $oneUser->cms_id = 0;
103 131 $this->save($oneUser);
@@ -111,11 +139,13 @@
111 139
112 140 return $results;
113 141 }
114 142
115 - private function handleSubscriptionFilter($settings, &$filters, &$query, &$queryCount, &$queryStatus)
143 + private function handleSubscriptionFilter(array $settings, array &$filters, string &$query, string &$queryCount, string &$queryStatus): void
116 144 {
117 - if (empty($settings['list'])) return;
145 + if (empty($settings['list'])) {
146 + return;
147 + }
118 148
119 149 $listJoin = ' JOIN #__acym_user_has_list AS list ON user.id = list.user_id AND list.list_id = '.intval($settings['list']).' ';
120 150
121 151 if ($settings['list_status'] === 'none') {
@@ -129,13 +159,15 @@
129 159 $queryCount .= $listJoin;
130 160 $queryStatus .= $listJoin;
131 161 }
132 162
133 - private function handleFrontend($settings, &$query, &$queryCount, &$queryStatus)
163 + private function handleFrontend(array $settings, string &$query, string &$queryCount, string &$queryStatus): void
134 164 {
135 165 if (!acym_isAdmin()) {
136 166 $settings['creator_id'] = acym_currentUserId();
137 - if (empty($settings['creator_id'])) $settings['creator_id'] = '-1';
167 + if (empty($settings['creator_id'])) {
168 + $settings['creator_id'] = '-1';
169 + }
138 170 }
139 171
140 172 if (!empty($settings['creator_id'])) {
141 173 $userGroups = acym_getGroupsByUser($settings['creator_id']);
@@ -149,9 +181,9 @@
149 181 $queryStatus .= $joinList;
150 182 }
151 183 }
152 184
153 - private function handleSearchFilter($settings, &$query, &$queryCount, &$queryStatus, &$filters)
185 + private function handleSearchFilter(array $settings, array &$filters, string &$query, string &$queryCount, string &$queryStatus): void
154 186 {
155 187 if (empty($settings['search'])) return;
156 188
157 189 $searchValue = acym_escapeDB('%'.$settings['search'].'%');
@@ -156,8 +188,9 @@
156 188
157 189 $searchValue = acym_escapeDB('%'.$settings['search'].'%');
158 190 $searchFilter = 'user.email LIKE '.$searchValue.' OR user.name LIKE '.$searchValue.' OR user.id = '.intval($settings['search']);
159 191
192 + // Search in visible custom fields
160 193 $listingFields = acym_loadResultArray('SELECT `id` FROM #__acym_field WHERE `'.(acym_isAdmin() ? 'back' : 'front').'end_listing` = 1');
161 194 if (!empty($listingFields)) {
162 195 $query = 'DISTINCT '.$query;
163 196 $join = ' LEFT JOIN #__acym_user_has_field AS userfield ON user.id = userfield.user_id AND userfield.field_id IN ('.implode(', ', $listingFields).') AND ';
@@ -191,9 +224,9 @@
191 224
192 225 $filters[] = $searchFilter;
193 226 }
194 227
195 - private function handleUserStatusFilter($settings, $queryStatus, &$filters)
228 + private function handleUserStatusFilter(array $settings, array &$filters, string $queryStatus): array
196 229 {
197 230 if (!empty($filters)) {
198 231 $queryStatus .= ' WHERE ('.implode(') AND (', $filters).')';
199 232 }
@@ -200,9 +233,9 @@
200 233
201 234 $usersPerStatusObject = acym_loadObjectList($queryStatus.' GROUP BY score', 'score');
202 235
203 236 $usersPerStatus = [];
204 - for ($i = 0 ; $i < 4 ; $i++) {
237 + for ($i = 0; $i < 4; $i++) {
205 238 $usersPerStatus[$i] = empty($usersPerStatusObject[$i]) ? 0 : $usersPerStatusObject[$i]->number;
206 239 }
207 240
208 241 if (!empty($settings['status'])) {
@@ -226,9 +259,9 @@
226 259 'unconfirmed' => $usersPerStatus[0] + $usersPerStatus[2],
227 260 ];
228 261 }
229 262
230 - private function handleEntitySelect($settings, &$filters)
263 + private function handleEntitySelect(array $settings, array &$filters): ?array
231 264 {
232 265 if (isset($settings['hiddenElements'])) {
233 266 if (empty($settings['hiddenElements'])) {
234 267 $filters[] = 'user.id IS NOT NULL';
@@ -246,8 +279,9 @@
246 279 $filters[] = 'user.id IN('.implode(',', $settings['onlyElements']).')';
247 280 }
248 281 }
249 282
283 + // For the entity select when selecting "show only selected users"
250 284 if (!empty($settings['showOnlySelected'])) {
251 285 if (!empty($settings['selectedUsers'])) {
252 286 acym_arrayToInteger($settings['selectedUsers']);
253 287 $filters[] = 'user.id IN('.implode(',', $settings['selectedUsers']).')';
@@ -258,9 +292,9 @@
258 292
259 293 return null;
260 294 }
261 295
262 - private function handleSegmentFilter($settings, &$filters, &$query, &$queryCount, &$queryStatus)
296 + private function handleSegmentFilter(array $settings, array &$filters, string &$query, string &$queryCount, string &$queryStatus): void
263 297 {
264 298 if (!array_key_exists('segment', $settings)) return;
265 299 if ($settings['segment'] == 0) return;
266 300
@@ -278,20 +312,23 @@
278 312 }
279 313 }
280 314 }
281 315
282 - $where = '';
316 + $whereBlocks = [];
283 317 $join = '';
284 - foreach ($automationHelpers as $index => $automationHelper) {
318 + foreach ($automationHelpers as $automationHelper) {
285 319 if (!empty($automationHelper->where)) {
286 - $where .= ' ('.implode(') and (', $automationHelper->where).')';
287 - if ($index != count($automationHelpers) - 1) $where .= ' OR ';
320 + $whereBlocks[] = ' ('.implode(') and (', $automationHelper->where).')';
288 321 }
289 - if (!empty($automationHelper->join)) $join .= ' JOIN '.implode(' JOIN ', $automationHelper->join);
290 - if (!empty($automationHelper->leftjoin)) $join .= ' LEFT JOIN '.implode(' LEFT JOIN ', $automationHelper->leftjoin);
322 + if (!empty($automationHelper->join)) {
323 + $join .= ' JOIN '.implode(' JOIN ', $automationHelper->join);
324 + }
325 + if (!empty($automationHelper->leftjoin)) {
326 + $join .= ' LEFT JOIN '.implode(' LEFT JOIN ', $automationHelper->leftjoin);
327 + }
291 328 }
292 - if (!empty($where)) {
293 - $filters[] = $where;
329 + if (!empty($whereBlocks)) {
330 + $filters[] = implode(' OR ', $whereBlocks);
294 331 }
295 332 $query .= $join;
296 333 $queryCount .= $join;
297 334 $queryStatus .= $join;
@@ -296,9 +333,9 @@
296 333 $queryCount .= $join;
297 334 $queryStatus .= $join;
298 335 }
299 336
300 - public function getJoinForQuery($joinType)
337 + public function getJoinForQuery(string $joinType): string
301 338 {
302 339 if (strpos($joinType, 'join_list') !== false) {
303 340 $listId = explode('-', $joinType);
304 341
@@ -307,23 +344,45 @@
307 344
308 345 return '';
309 346 }
310 347
311 - public function getOneByCMSId(?int $id)
348 + public function getOneByCMSId(?int $id): ?object
312 349 {
313 - $query = 'SELECT * FROM #__acym_user WHERE cms_id = '.intval($id);
350 + if (empty($id)) {
351 + return null;
352 + }
314 353
315 - return acym_loadObject($query);
354 + $user = acym_loadObject('SELECT * FROM #__acym_user WHERE cms_id = '.intval($id));
355 +
356 + return empty($user) ? null : $user;
316 357 }
317 358
318 - public function getOneByEmail(?string $email)
359 + public function getOneByEmail(?string $email, bool $autoAdd = false): ?object
319 360 {
320 - $query = 'SELECT * FROM #__acym_user WHERE email = '.acym_escapeDB($email);
361 + if (!acym_isValidEmail($email)) {
362 + return null;
363 + }
321 364
322 - return acym_loadObject($query);
365 + $user = acym_loadObject('SELECT * FROM #__acym_user WHERE email = '.acym_escapeDB($email));
366 +
367 + if (empty($user) && $autoAdd) {
368 + $newUser = new \stdClass();
369 + $newUser->email = $email;
370 + $this->sendConf = false;
371 + $this->checkVisitor = false;
372 +
373 + $userId = $this->save($newUser);
374 + if (empty($userId)) {
375 + return null;
376 + }
377 +
378 + $user = $this->getOneById($userId);
379 + }
380 +
381 + return empty($user) ? null : $user;
323 382 }
324 383
325 - public function getByColumnValue($column, $value)
384 + public function getByColumnValue(string $column, string $value): array
326 385 {
327 386 $userColumns = acym_getColumns('user');
328 387 if (!in_array($column, $userColumns)) return [];
329 388
@@ -332,20 +391,20 @@
332 391 return acym_loadObjectList($query, $this->pkey);
333 392 }
334 393
335 394 public function getUserSubscriptionById(
336 - $userId,
337 - $key = 'id',
338 - $includeManagement = false,
339 - $visible = false,
340 - $needTranslation = false,
341 - $sortBySubscription = false,
342 - $mailId = 0,
343 - $campaignOnly = false
344 - ) {
395 + int $userId,
396 + string $key = 'id',
397 + bool $includeManagement = false,
398 + bool $visible = false,
399 + bool $needTranslation = false,
400 + bool $sortBySubscription = false,
401 + int $mailId = 0,
402 + bool $campaignOnly = false
403 + ): array {
345 404 $mailType = null;
346 405 if (!empty($mailId)) {
347 - $mailType = acym_loadResult('SELECT type FROM #__acym_mail WHERE id = '.intval($mailId));
406 + $mailType = (new MailClass())->getMailType(intval($mailId));
348 407 }
349 408
350 409 $query = 'SELECT list.id, list.translation, list.name, list.display_name, list.color, list.active, list.visible, list.description, userlist.status, userlist.subscription_date, userlist.unsubscribe_date
351 410 FROM #__acym_list AS list
@@ -361,9 +420,10 @@
361 420 if (!$includeManagement && !empty($mailType) && $mailType === MailClass::TYPE_FOLLOWUP) {
362 421 $query .= ' AND list.type = '.acym_escapeDB(ListClass::LIST_TYPE_FOLLOWUP);
363 422 } elseif (!$includeManagement) {
364 423 $types = [ListClass::LIST_TYPE_STANDARD, ListClass::LIST_TYPE_FOLLOWUP];
365 - $query .= ' AND list.type in ("'.implode('","', $types).'")';
424 + $types = array_map('acym_escapeDB', $types);
425 + $query .= ' AND list.type IN ('.implode(',', $types).')';
366 426 }
367 427
368 428 if ($visible) {
369 429 $query .= ' AND list.visible = 1';
@@ -382,9 +442,9 @@
382 442
383 443 return $lists;
384 444 }
385 445
386 - public function getUserStandardListIdById(int $userId)
446 + public function getUserStandardListIdById(int $userId): array
387 447 {
388 448 $query = 'SELECT list.id FROM #__acym_list AS list
389 449 JOIN #__acym_user_has_list AS user_list ON list.id = user_list.list_id AND user_list.status = 1 AND user_list.user_id = '.$userId.'
390 450 WHERE list.visible = 1 AND list.type = '.acym_escapeDB(ListClass::LIST_TYPE_STANDARD);
@@ -391,9 +451,9 @@
391 451
392 452 return acym_loadResultArray($query);
393 453 }
394 454
395 - public function getAllListsUserSubscriptionById($userId, $key = 'id', $needTranslation = false)
455 + public function getAllListsUserSubscriptionById(int $userId, string $key = 'id', bool $needTranslation = false): array
396 456 {
397 457 $query = 'SELECT list.id, list.translation, list.name, list.color, list.active, list.visible, userlist.status, userlist.subscription_date, userlist.unsubscribe_date
398 458 FROM #__acym_list AS list
399 459 LEFT JOIN #__acym_user_has_list AS userlist
@@ -409,21 +469,26 @@
409 469
410 470 return $lists;
411 471 }
412 472
413 - public function getUsersSubscriptionsByIds($usersId, $creatorId = 0)
473 + public function getUsersSubscriptionsByIds(array $userIds, int $creatorId = 0): array
414 474 {
475 + acym_arrayToInteger($userIds);
415 476 $query = 'SELECT `list`.`id`, `list`.`color`, `list`.`name`, `userlist`.`user_id`, `userlist`.`status`
416 477 FROM #__acym_list AS `list`
417 478 JOIN #__acym_user_has_list AS `userlist`
418 479 ON `list`.`id` = `userlist`.`list_id`
419 - WHERE `userlist`.`user_id` IN ('.implode(',', $usersId).')
480 + WHERE `userlist`.`user_id` IN ('.implode(',', $userIds).')
420 481 AND `list`.`type` = '.acym_escapeDB(ListClass::LIST_TYPE_STANDARD);
421 482
422 483 if (!empty($creatorId)) {
423 484 $userGroups = acym_getGroupsByUser($creatorId);
424 - $groupCondition = '`list`.`access` LIKE "%,'.implode(',%" OR `list`.`access` LIKE "%,', $userGroups).',%"';
425 - $query .= ' AND (`list`.`cms_user_id` = '.intval($creatorId).' OR '.$groupCondition.')';
485 +
486 + $safeGroups = [];
487 + foreach ($userGroups as $group) {
488 + $safeGroups[] = acym_escapeDB('%,'.$group.',%');
489 + }
490 + $query .= ' AND (`list`.`cms_user_id` = '.intval($creatorId).' OR `list`.`access` LIKE '.implode(' OR `list`.`access` LIKE ', $safeGroups).')';
426 491 }
427 492 $query .= ' ORDER BY `userlist`.`status` DESC, `list`.`id` ASC';
428 493
429 494 return acym_loadObjectList($query);
@@ -435,9 +500,9 @@
435 500
436 501 return empty($numberOfUsers) ? 0 : $numberOfUsers;
437 502 }
438 503
439 - public function getSubscriptionStatus($userId, $listIds = [], $wantedStatus = null)
504 + public function getSubscriptionStatus(int $userId, array $listIds = [], ?int $wantedStatus = null): array
440 505 {
441 506 $query = 'SELECT status, list_id
442 507 FROM #__acym_user_has_list
443 508 WHERE user_id = '.intval($userId);
@@ -452,9 +517,16 @@
452 517
453 518 return acym_loadObjectList($query, 'list_id');
454 519 }
455 520
456 - public function identify(bool $onlyValue = false, $idName = null, $keyName = null)
521 + /**
522 + * Identify a user from the acy user_id and key of the e-mail
523 + *
524 + * @param bool $onlyValue only return the user, don't display errors
525 + *
526 + * @return ?object the identified user or null if not found
527 + */
528 + public function identify(bool $onlyValue = false, ?string $idName = null, ?string $keyName = null): ?object
457 529 {
458 530 $id = acym_getVar('int', empty($idName) ? 'id' : $idName, 0);
459 531 $key = acym_getVar('string', empty($keyName) ? 'key' : $keyName, '');
460 532
@@ -463,8 +535,10 @@
463 535 $key = acym_getVar('string', 'key', 0);
464 536 }
465 537
466 538 if (empty($id) || empty($key)) {
539 + //Check if the user is not already in the session...
540 + //Check if the user is not loaded in...
467 541 $currentUserid = acym_currentUserId();
468 542 if (!empty($currentUserid)) {
469 543 return $this->getOneByCMSId($currentUserid);
470 544 }
@@ -471,9 +545,9 @@
471 545 if (!$onlyValue) {
472 546 acym_enqueueMessage(acym_translation('ACYM_LOGIN'), 'error');
473 547 }
474 548
475 - return false;
549 + return null;
476 550 }
477 551
478 552 $userIdentified = acym_loadObject('SELECT * FROM #__acym_user WHERE `id` = '.intval($id).' AND `key` = '.acym_escapeDB($key));
479 553
@@ -484,29 +558,28 @@
484 558 if (!$onlyValue) {
485 559 acym_enqueueMessage(acym_translation('ACYM_USER_NOT_FOUND'), 'error');
486 560 }
487 561
488 - return false;
562 + return null;
489 563 }
490 564
491 - public function subscribe($userIds, $addLists, $trigger = true, $forceFront = false)
565 + public function subscribe($userIds, array $addLists, bool $trigger = true, bool $forceFront = false): bool
492 566 {
493 567 if (empty($addLists)) {
494 568 return false;
495 569 }
570 + acym_arrayToInteger($addLists);
496 571
572 + //TODO was previously shown as an int in the dev documentation, hence not adding the type yet
497 573 if (!is_array($userIds)) {
498 574 $userIds = [$userIds];
499 575 }
576 + acym_arrayToInteger($userIds);
500 577
501 - if (!is_array($addLists)) {
502 - $addLists = [$addLists];
503 - }
504 -
505 578 $listClass = new ListClass();
506 579 $historyClass = new HistoryClass();
507 580
508 - $confirmationRequired = $this->config->get('require_confirmation', 1);
581 + $confirmationRequired = !empty($this->config->get('require_confirmation', 1));
509 582 $subscribedToLists = false;
510 583 $historyData = acym_translationSprintf('ACYM_LISTS_NUMBERS', implode(', ', $addLists));
511 584
512 585 foreach ($userIds as $userId) {
@@ -518,10 +591,9 @@
518 591 $currentlyUnsubscribed = [];
519 592 foreach ($currentSubscription as $oneList) {
520 593 if ($oneList->status == 1) {
521 594 $currentlySubscribed[$oneList->id] = $oneList;
522 - }
523 - if ($oneList->status == 0) {
595 + } else {
524 596 $currentlyUnsubscribed[$oneList->id] = $oneList;
525 597 }
526 598 }
527 599
@@ -527,8 +599,9 @@
527 599
528 600 $subscribedLists = [];
529 601 $listsNotExisting = [];
530 602 foreach ($addLists as $oneListId) {
603 + // The user is already subscribed
531 604 if (empty($oneListId) || !empty($currentlySubscribed[$oneListId])) {
532 605 continue;
533 606 }
534 607
@@ -541,13 +614,15 @@
541 614 $subscription = new \stdClass();
542 615 $subscription->user_id = $userId;
543 616 $subscription->list_id = $oneListId;
544 617 $subscription->status = 1;
545 - $subscription->subscription_date = date('Y-m-d H:i:s', time() - date('Z'));
618 + $subscription->subscription_date = gmdate('Y-m-d H:i:s', time());
546 619
547 620 if (empty($currentSubscription[$oneListId])) {
621 + // The user isn't already subscribed, we subscribe him!
548 622 acym_insertObject('#__acym_user_has_list', $subscription);
549 623 } elseif (!empty($currentlyUnsubscribed[$oneListId])) {
624 + // The user re-subscribes or we have to confirm the subscription
550 625 acym_updateObject('#__acym_user_has_list', $subscription, ['user_id', 'list_id']);
551 626 }
552 627
553 628 $subscribedLists[] = $oneListId;
@@ -563,9 +638,9 @@
563 638 if ($trigger) {
564 639 acym_trigger('onAcymAfterUserSubscribe', [&$user, &$subscribedLists]);
565 640 }
566 641
567 - if ($this->sendWelcomeEmail && ($confirmationRequired == 0 || $user->confirmed == 1) && $user->active == 1) {
642 + if ($this->sendWelcomeEmail && (!$confirmationRequired || $user->confirmed == 1) && $user->active == 1) {
568 643 $listClass->sendWelcome($userId, $subscribedLists, $forceFront);
569 644 }
570 645 }
571 646
@@ -571,9 +646,9 @@
571 646
572 647 return $subscribedToLists;
573 648 }
574 649
575 - private function registerUnsubUser($userIds)
650 + private function registerUnsubUser(array $userIds): void
576 651 {
577 652 $mailId = acym_getVar('int', 'mail_id', 0);
578 653 if (empty($mailId)) return;
579 654
@@ -583,42 +658,52 @@
583 658 $countUnsubscribe = 0;
584 659
585 660 foreach ($userIds as $id) {
586 661 $userStat = $userStatClass->getOneByMailAndUserId($mailId, $id);
662 + // We didn't send this email to this user, something is wrong
587 663 if (empty($userStat)) continue;
588 664
589 - $newUserStat = [
590 - 'mail_id' => $mailId,
591 - 'user_id' => $id,
592 - 'unsubscribe' => empty($userStat->unsubscribe) ? 1 : $userStat->unsubscribe + 1,
593 - ];
665 + $newUserStat = new \stdClass();
666 + $newUserStat->mail_id = $mailId;
667 + $newUserStat->user_id = $id;
668 + $newUserStat->unsubscribe = empty($userStat->unsubscribe) ? 1 : $userStat->unsubscribe + 1;
594 669
595 - if (empty($userStat->unsubscribe)) $countUnsubscribe++;
670 + if (empty($userStat->unsubscribe)) {
671 + $countUnsubscribe++;
672 + }
596 673
597 674 $userStatClass->save($newUserStat);
598 675 }
599 676
600 - $mailStat = [
601 - 'mail_id' => $mailId,
602 - 'unsubscribe_total' => $countUnsubscribe,
603 - ];
677 + $mailStat = new \stdClass();
678 + $mailStat->mail_id = $mailId;
679 + $mailStat->unsubscribe_total = $countUnsubscribe;
604 680
605 681 $mailStatClass->save($mailStat);
606 682 }
607 683
608 - public function unsubscribe($userIds, $lists)
684 + public function unsubscribe($userIds, array $lists): bool
609 685 {
610 - if (empty($lists)) return false;
686 + if (empty($lists)) {
687 + return false;
688 + }
611 689
612 - if (!is_array($userIds)) $userIds = [$userIds];
613 - if (!is_array($lists)) $lists = [$lists];
690 + //TODO was previously shown as an int in the dev documentation, hence not adding the type yet
691 + if (!is_array($userIds)) {
692 + $userIds = [$userIds];
693 + }
694 + acym_arrayToInteger($userIds);
695 + acym_arrayToInteger($lists);
614 696
615 697 $listClass = new ListClass();
616 698 $unsubscribedFromLists = false;
617 699 foreach ($userIds as $userId) {
618 700 $user = $this->getOneById($userId);
619 - if (empty($user)) continue;
620 701
702 + if (empty($user)) {
703 + continue;
704 + }
705 +
621 706 $currentSubscription = $this->getUserSubscriptionById($userId);
622 707
623 708 $currentlySubscribed = [];
624 709 $currentlyUnsubscribed = [];
@@ -632,11 +717,17 @@
632 717
633 718 $unsubscribedLists = [];
634 719 $listsNotExisting = [];
635 720 foreach ($lists as $oneListId) {
636 - if (empty($oneListId) || !empty($currentlyUnsubscribed[$oneListId])) continue;
721 + // The user is already unsubscribed
722 + if (empty($oneListId) || !empty($currentlyUnsubscribed[$oneListId])) {
723 + continue;
724 + }
637 725
638 - if (empty($currentlySubscribed[$oneListId])) continue;
726 + // Don't unsubscribe from non-subscribed lists
727 + if (empty($currentlySubscribed[$oneListId])) {
728 + continue;
729 + }
639 730
640 731 $list = $listClass->getOneById($oneListId);
641 732 if (empty($list)) {
642 733 $listsNotExisting[] = $oneListId;
@@ -646,12 +737,14 @@
646 737 $subscription = new \stdClass();
647 738 $subscription->user_id = $userId;
648 739 $subscription->list_id = $oneListId;
649 740 $subscription->status = 0;
650 - $subscription->unsubscribe_date = date('Y-m-d H:i:s', time() - date('Z'));
741 + $subscription->unsubscribe_date = gmdate('Y-m-d H:i:s', time());
651 742 if (empty($currentSubscription[$oneListId])) {
743 + // The user isn't already subscribed, we unsubscribe him directly
652 744 acym_insertObject('#__acym_user_has_list', $subscription);
653 745 } else {
746 + // The user unsubscribed from a list
654 747 acym_updateObject('#__acym_user_has_list', $subscription, ['user_id', 'list_id']);
655 748 }
656 749
657 750 $unsubscribedLists[] = $oneListId;
@@ -689,44 +782,40 @@
689 782 $this->sendNotification($user->id, 'acy_notification_unsub', ['lists' => implode(', ', $unsubscribedLists)]);
690 783 }
691 784 }
692 785
693 - if ($unsubscribedFromLists) $this->registerUnsubUser($userIds);
786 + if ($unsubscribedFromLists) {
787 + $this->registerUnsubUser($userIds);
788 + }
694 789
695 790 return $unsubscribedFromLists;
696 791 }
697 792
698 - public function unsubscribeOnSubscriptions($userId, $listIds)
793 + public function unsubscribeOnSubscriptions(int $userId, array $listIds): bool
699 794 {
700 795 if (empty($listIds)) {
701 796 return false;
702 797 }
703 798
704 - if (!is_array($listIds)) {
705 - $listIds = [$listIds];
706 - }
707 799 acym_arrayToInteger($listIds);
708 800
709 801 $subscribedLists = acym_loadResultArray('SELECT list_id FROM #__acym_user_has_list WHERE user_id = '.intval($userId).' AND list_id IN ('.implode(',', $listIds).')');
710 802
711 - if (!empty($subscribedLists)) $this->unsubscribe($userId, $subscribedLists);
803 + if (!empty($subscribedLists)) $this->unsubscribe([$userId], $subscribedLists);
712 804
713 805 return true;
714 806 }
715 807
716 - public function removeSubscription($userIds, $listIds = [])
808 + public function removeSubscription(array $userIds, array $listIds = []): void
717 809 {
718 - if (!is_array($userIds)) {
719 - $userIds = [$userIds];
810 + if (empty($listIds) || empty($userIds)) {
811 + return;
720 812 }
721 813
722 - if (!is_array($listIds) || empty($listIds) || empty($userIds)) {
723 - return false;
724 - }
725 -
814 + acym_arrayToInteger($userIds);
726 815 acym_arrayToInteger($listIds);
727 816
728 - return acym_query(
817 + acym_query(
729 818 'DELETE FROM #__acym_user_has_list
730 819 WHERE user_id IN ('.implode(',', $userIds).')
731 820 AND list_id IN ('.implode(',', $listIds).')'
732 821 );
@@ -731,11 +820,11 @@
731 820 AND list_id IN ('.implode(',', $listIds).')'
732 821 );
733 822 }
734 823
735 - public function onlyManageableUsers(&$elements)
824 + public function onlyManageableUsers(array &$elements): void
736 825 {
737 - if (acym_isAdmin()) {
826 + if (acym_isAdmin() && acym_isAllowed('users')) {
738 827 return;
739 828 }
740 829
741 830 $listClass = new ListClass();
@@ -760,14 +849,10 @@
760 849 $elements[] = $currentUserId;
761 850 }
762 851 }
763 852
764 - public function delete($elements, $forceDelete = false)
853 + public function delete(array $elements, bool $forceDelete = false): int
765 854 {
766 - if (!is_array($elements)) {
767 - $elements = [$elements];
768 - }
769 -
770 855 acym_arrayToInteger($elements);
771 856 $this->onlyManageableUsers($elements);
772 857
773 858 if (empty($elements)) {
@@ -788,8 +873,16 @@
788 873 acym_query('DELETE FROM #__acym_scenario_queue WHERE scenario_process_id IN ('.implode(',', $scenarioProcessIds).')');
789 874 acym_query('DELETE FROM #__acym_scenario_process WHERE id IN ('.implode(',', $scenarioProcessIds).')');
790 875 }
791 876
877 + $uploadFolder = trim(acym_cleanPath(html_entity_decode(acym_getFilesFolder(true))), DS.' ').DS;
878 + foreach ($elements as $userId) {
879 + $folderPath = acym_cleanPath(ACYM_ROOT.$uploadFolder.'userfiles'.DS.$userId.DS);
880 + if (is_dir($folderPath)) {
881 + acym_deleteFolder($folderPath);
882 + }
883 + }
884 +
792 885 return parent::delete($elements);
793 886 } else {
794 887 $listClass = new ListClass();
795 888 $manageableLists = $listClass->getManageableLists();
@@ -809,12 +902,12 @@
809 902 return count($elements);
810 903 }
811 904 }
812 905
813 - public function save($user, $customFields = [], $ajax = false)
906 + public function save(object $user, array $customFields = [], bool $ajax = false): ?int
814 907 {
815 908 if (empty($user->email) && empty($user->id)) {
816 - return false;
909 + return null;
817 910 }
818 911
819 912 if (isset($user->email)) {
820 913 $user->email = strtolower($user->email);
@@ -820,9 +913,9 @@
820 913 $user->email = strtolower($user->email);
821 914 if (!acym_isValidEmail($user->email)) {
822 915 $this->errors[] = acym_translation('ACYM_VALID_EMAIL');
823 916
824 - return false;
917 + return null;
825 918 }
826 919 }
827 920
828 921 if (empty($user->id)) {
@@ -829,9 +922,14 @@
829 922 if (!isset($user->active)) {
830 923 $user->active = 1;
831 924 }
832 925
926 + if (!isset($user->tracking)) {
927 + $user->tracking = $this->config->get('dont_track_by_default', 0) ? 0 : 1;
928 + }
929 +
833 930 if (empty($user->language)) {
931 + // Take the user account's language
834 932 if (!acym_isAdmin()) {
835 933 $cmsUserLanguage = acym_getCmsUserLanguage();
836 934 }
837 935
@@ -836,11 +934,10 @@
836 934 }
837 935
838 936 if (!empty($cmsUserLanguage)) {
839 937 $user->language = $cmsUserLanguage;
840 - } elseif (acym_isMultilingual()) {
841 - $configUserLanguage = $this->config->get('multilingual_user_default', 'current_language');
842 - $user->language = $configUserLanguage === 'current_language' ? acym_getLanguageTag() : $configUserLanguage;
938 + } else {
939 + $user->language = acym_getDefaultUserLanguage();
843 940 }
844 941 }
845 942
846 943 $currentUserid = acym_currentUserId();
@@ -846,11 +943,12 @@
846 943 $currentUserid = acym_currentUserId();
847 944 $currentEmail = acym_currentUserEmail();
848 945 $allowVisitors = $this->config->get('allow_visitor', 1) == 1;
849 946 if ($this->checkVisitor && !acym_isAdmin() && !$allowVisitors && (empty($currentUserid) || strtolower($currentEmail) !== $user->email)) {
947 + //We don't accept the subscription as either the user is not logged in or it's not the good one
850 948 $this->errors[] = acym_translation('ACYM_ONLY_LOGGED');
851 949
852 - return false;
950 + return null;
853 951 }
854 952
855 953 if (empty($user->name) && $this->config->get('generate_name', 1)) {
856 954 $user->name = ucwords(trim(str_replace(['.', '_', ')', ',', '(', '-', 1, 2, 3, 4, 5, 6, 7, 8, 9, 0], ' ', substr($user->email, 0, strpos($user->email, '@')))));
@@ -856,17 +954,19 @@
856 954 $user->name = ucwords(trim(str_replace(['.', '_', ')', ',', '(', '-', 1, 2, 3, 4, 5, 6, 7, 8, 9, 0], ' ', substr($user->email, 0, strpos($user->email, '@')))));
857 955 }
858 956
859 957 $source = acym_getVar('string', 'acy_source', '');
860 - if (empty($user->source) && !empty($source)) $user->source = $source;
958 + if (empty($user->source) && !empty($source)) {
959 + $user->source = $source;
960 + }
861 961
862 - if (empty($user->key)) $user->key = acym_generateKey(14);
962 + $user->key = acym_generateKey(14);
863 963
864 - $user->creation_date = date('Y-m-d H:i:s', time() - date('Z'));
964 + $user->creation_date = gmdate('Y-m-d H:i:s', time());
865 965 } elseif (!empty($user->confirmed)) {
866 966 $oldUser = $this->getOneByIdWithCustomFields($user->id);
867 967 if (!empty($oldUser) && empty($oldUser['confirmed'])) {
868 - $user->confirmation_date = date('Y-m-d H:i:s', time() - date('Z'));
968 + $user->confirmation_date = gmdate('Y-m-d H:i:s', time());
869 969 $user->confirmation_ip = acym_getIP();
870 970
871 971 if ($this->triggers) {
872 972 acym_trigger('onAcymAfterUserConfirm', [&$user]);
@@ -875,30 +975,53 @@
875 975 } else {
876 976 $oldUser = $this->getOneByIdWithCustomFields($user->id);
877 977 }
878 978
979 + if (!empty($user->id) && isset($user->active) && !empty($oldUser)) {
980 + $wasActive = isset($oldUser['active']) ? (int)$oldUser['active'] : 1;
981 + $isNowActive = (int)$user->active;
982 + if ($wasActive === 1 && $isNowActive === 0) {
983 + $user->deactivation_date = acym_date('now', 'Y-m-d H:i:s');
984 + } elseif ($wasActive === 0 && $isNowActive === 1) {
985 + $user->deactivation_date = null;
986 + }
987 + }
988 +
879 989 foreach ($user as $oneAttribute => $value) {
880 - if (empty($value)) continue;
990 + if (empty($value)) {
991 + continue;
992 + }
881 993
882 994 $oneAttribute = trim(strtolower($oneAttribute));
883 - if (!in_array($oneAttribute, $this->restrictedFields)) {
884 - $user->$oneAttribute = strip_tags($value);
995 + if (!in_array($oneAttribute, self::RESTRICTED_FIELDS)) {
996 + $user->$oneAttribute = acym_stripTags($value);
885 997 }
886 998
887 - if (is_numeric($user->$oneAttribute)) continue;
999 + // Convert into utf-8 in case of it's not already
1000 + // Double test on UTF-8 because the preg_match returns an error on string longer than 200 characters
1001 + if (is_numeric($user->$oneAttribute)) {
1002 + continue;
1003 + }
888 1004
889 1005 if (function_exists('mb_detect_encoding')) {
890 1006 if (mb_detect_encoding($user->$oneAttribute, 'UTF-8', true) !== 'UTF-8') {
891 1007 $user->$oneAttribute = acym_utf8Encode($user->$oneAttribute);
892 1008 }
893 - } elseif (!preg_match(
894 - '%^(?:[\x09\x0A\x0D\x20-\x7E]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$%xs',
895 - $user->$oneAttribute
896 - )) {
1009 + } elseif (
1010 + !preg_match(
1011 + '%^(?:[\x09\x0A\x0D\x20-\x7E]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$%xs',
1012 + $user->$oneAttribute
1013 + )
1014 + ) {
897 1015 $user->$oneAttribute = acym_utf8Encode($user->$oneAttribute);
898 1016 }
899 1017 }
900 1018
1019 + // Prevent SQL error on flags that aren't cleaned
1020 + if (!empty($user->automation) && strlen($user->automation) > 20) {
1021 + $user->automation = '';
1022 + }
1023 +
901 1024 if (empty($user->id)) {
902 1025 if (empty($user->cms_id) && !empty($user->email)) {
903 1026 global $acymCmsUserVars;
904 1027 $userCmsID = acym_loadResult(
@@ -905,13 +1028,14 @@
905 1028 'SELECT '.acym_secureDBColumn($acymCmsUserVars->id).' FROM '.$acymCmsUserVars->table.' WHERE '.acym_secureDBColumn(
906 1029 $acymCmsUserVars->email
907 1030 ).' = '.acym_escapeDB($user->email)
908 1031 );
1032 +
909 1033 if (!empty($userCmsID)) $user->cms_id = $userCmsID;
910 1034 }
911 1035 if ($this->triggers) {
912 1036 $result = acym_trigger('onAcymBeforeUserCreate', [&$user]);
913 - if (in_array(false, $result)) {
1037 + if (is_array($result) && in_array(false, $result)) {
914 1038 $acycheckerError = acym_getVar('string', 'acychecker_error');
915 1039 if (!empty($acycheckerError)) {
916 1040 if ($ajax) {
917 1041 $this->errors[] = $acycheckerError;
@@ -919,9 +1043,9 @@
919 1043 acym_enqueueMessage($acycheckerError, 'error');
920 1044 }
921 1045 }
922 1046
923 - return false;
1047 + return null;
924 1048 }
925 1049 }
926 1050 } else {
927 1051 if ($this->triggers) {
@@ -930,8 +1054,9 @@
930 1054 }
931 1055
932 1056 $userID = parent::save($user);
933 1057
1058 + // Save custom fields if there are any
934 1059 $fieldClass = new FieldClass();
935 1060 if (!empty($customFields)) {
936 1061 foreach ($customFields as $key => $value) {
937 1062 if (!is_string($value)) {
@@ -937,8 +1062,9 @@
937 1062 if (!is_string($value)) {
938 1063 continue;
939 1064 }
940 1065
1066 + // Remove most of the existing emojis that are not supported
941 1067 $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $value);
942 1068
943 1069 $field = $fieldClass->getOneById($key);
944 1070 if (empty($field) || $field->type !== 'textarea') {
@@ -972,12 +1098,12 @@
972 1098
973 1099 return $userID;
974 1100 }
975 1101
976 - public function saveForm($ajax = false)
1102 + public function saveForm(bool $ajax = false): bool
977 1103 {
978 - $allowUserModifications = (bool)($this->config->get('allow_modif', 'data') == 'all') || $this->allowModif;
979 - $allowSubscriptionModifications = (bool)($this->config->get('allow_modif', 'data') != 'none') || $this->allowModif;
1104 + $allowUserModifications = $this->config->get('allow_modif', 'data') === 'all' || $this->allowModif;
1105 + $allowSubscriptionModifications = $this->config->get('allow_modif', 'data') !== 'none' || $this->allowModif;
980 1106
981 1107 $user = new \stdClass();
982 1108 $connectedUser = $this->identify(true, 'userId', 'userKey');
983 1109
@@ -983,12 +1109,19 @@
983 1109
984 1110 $userData = acym_getVar('array', 'user', []);
985 1111 if (!empty($userData)) {
986 1112 foreach ($userData as $attribute => $value) {
1113 + if (!in_array($attribute, self::FORM_ALLOWED_FIELDS, true)) {
1114 + continue;
1115 + }
987 1116 $user->$attribute = $value;
988 1117 }
989 1118 }
990 1119
1120 + if (!empty($connectedUser->id)) {
1121 + $user->id = $connectedUser->id;
1122 + }
1123 +
991 1124 if (empty($user->email)) {
992 1125 if (!empty($connectedUser->email)) {
993 1126 $user->email = $connectedUser->email;
994 1127 }
@@ -1010,9 +1143,11 @@
1010 1143 $allowUserModifications = true;
1011 1144 $allowSubscriptionModifications = true;
1012 1145 }
1013 1146
1147 + // Check if it already exists...
1014 1148 if (!empty($user->email)) {
1149 + // Do we already have a user with this same e-mail address?
1015 1150 if (empty($user->id)) {
1016 1151 $user->id = 0;
1017 1152 }
1018 1153 $existUser = acym_loadObject('SELECT * FROM #__acym_user WHERE email = '.acym_escapeDB($user->email).' AND id != '.intval($user->id));
@@ -1026,8 +1161,10 @@
1026 1161 }
1027 1162 }
1028 1163 }
1029 1164
1165 + // Did the user ask for its e-mail address to be modified?
1166 + // If so, we set it as not confirmed automatically.
1030 1167 if (!empty($user->id) && !empty($user->email)) {
1031 1168 $existUser = $this->getOneById($user->id);
1032 1169 if (trim(strtolower($user->email)) != strtolower($existUser->email)) {
1033 1170 $user->confirmed = 0;
@@ -1033,21 +1170,27 @@
1033 1170 $user->confirmed = 0;
1034 1171 }
1035 1172 }
1036 1173
1174 + $fromProfile = acym_getVar('int', 'acyprofile', 0) == 1;
1175 +
1037 1176 $this->newUser = empty($user->id);
1038 1177 if (empty($user->id) || $allowUserModifications) {
1039 - if (isset($user->confirmed) && $user->confirmed != 1) {
1178 + if ($this->newUser && $this->config->get('require_confirmation', 1) == 1) {
1040 1179 $user->confirmed = 0;
1041 1180 }
1042 - if (isset($user->active) && $user->active != 1) {
1043 - $user->active = 0;
1181 +
1182 + if ($fromProfile && !(isset($user->confirmed) && empty($user->confirmed))) {
1183 + $this->sendConf = false;
1044 1184 }
1185 +
1186 + // Get custom fields to save them if exist
1045 1187 $customFieldData = acym_getVar('array', 'customField', []);
1046 1188 $id = $this->save($user, $customFieldData, $ajax);
1047 1189 $allowSubscriptionModifications = true;
1048 1190 } else {
1049 1191 $id = $user->id;
1192 + //We didn't save the user... but we should check the confirm field anyway
1050 1193 if (isset($user->confirmed) && empty($user->confirmed)) {
1051 1194 $this->sendConfirmation($id);
1052 1195 }
1053 1196 }
@@ -1059,11 +1202,12 @@
1059 1202
1060 1203 if (!acym_isAdmin()) {
1061 1204 $hiddenlistsString = acym_getVar('string', 'hiddenlists', '');
1062 1205 $hiddenlists = explode(',', $hiddenlistsString);
1063 - acym_arrayToInteger($hiddenlists);
1064 1206 $visibleSubscription = acym_getVar('array', 'subscription', []);
1065 1207 $subscribeLists = array_merge($hiddenlists, $visibleSubscription);
1208 + acym_arrayToInteger($subscribeLists);
1209 +
1066 1210 if (!empty($subscribeLists)) {
1067 1211 foreach ($subscribeLists as $oneListId) {
1068 1212 $formData['listsub'][$oneListId] = ['status' => 1];
1069 1213 }
@@ -1069,10 +1213,8 @@
1069 1213 }
1070 1214 }
1071 1215 }
1072 1216
1073 - $fromProfile = acym_getVar('int', 'acyprofile', 0) == 1;
1074 -
1075 1217 if (empty($formData['listsub'])) {
1076 1218 if (!$fromProfile) $this->sendNotification($id, 'acy_notification_subform');
1077 1219
1078 1220 $this->sendNotification(
@@ -1099,9 +1241,9 @@
1099 1241 $unsubLists[] = $listID;
1100 1242 }
1101 1243 }
1102 1244
1103 - $this->subscribed = $this->subscribe($id, $addLists);
1245 + $this->subscribed = $this->subscribe([$id], $addLists);
1104 1246
1105 1247 if (!$fromProfile) $this->sendNotification($id, 'acy_notification_subform');
1106 1248
1107 1249 $this->sendNotification(
@@ -1108,14 +1250,14 @@
1108 1250 $id,
1109 1251 $this->newUser ? 'acy_notification_create' : 'acy_notification_profile'
1110 1252 );
1111 1253
1112 - if (!$this->newUser) $this->unsubscribe($id, $unsubLists);
1254 + if (!$this->newUser) $this->unsubscribe([$id], $unsubLists);
1113 1255
1114 1256 return true;
1115 1257 }
1116 1258
1117 - public function sendConfirmation($userID)
1259 + public function sendConfirmation(int $userID): void
1118 1260 {
1119 1261 if (!$this->forceConf && !$this->sendConf) {
1120 1262 return;
1121 1263 }
@@ -1132,25 +1274,52 @@
1132 1274
1133 1275 $mailerHelper = new MailerHelper();
1134 1276 $mailerHelper->report = (bool)$this->config->get('confirm_message', 0);
1135 1277
1278 + //TODO $mailerHelper->addParam('user:subscription', implode('<br/>', $subscription));
1136 1279 $mailClass = new MailClass();
1137 1280 $confirmationEmail = $mailClass->getOneByName('acy_confirm');
1138 - $this->confirmationSentSuccess = !empty($confirmationEmail) && $mailerHelper->sendOne($confirmationEmail->id, $myUser);
1281 + $this->confirmationSentSuccess = !empty($confirmationEmail) && $mailerHelper->sendOne($confirmationEmail->id, $myUser->id);
1139 1282 $this->confirmationSentError = $mailerHelper->reportMessage;
1140 1283 }
1141 1284
1142 - public function deactivate($userId)
1285 + public function setInactive(array $elements): void
1143 1286 {
1144 - acym_query('UPDATE `#__acym_user` SET `active` = 0 WHERE `id` = '.intval($userId));
1287 + $this->deactivate($elements);
1145 1288 }
1146 1289
1147 - public function confirm($userId)
1290 + public function deactivate(array $userIds): void
1148 1291 {
1292 + $userIds = array_map('intval', $userIds);
1293 + $userIds = array_filter($userIds);
1294 +
1295 + if (empty($userIds)) return;
1296 +
1297 + acym_query('UPDATE `#__acym_user` SET `active` = 0, `deactivation_date` = UTC_TIMESTAMP() WHERE `id` IN ('.implode(',', $userIds).')');
1298 + }
1299 +
1300 + public function setActive(array $elements): void
1301 + {
1302 + $this->reactivate($elements);
1303 + }
1304 +
1305 + public function reactivate(array $userIds): void
1306 + {
1307 + $userIds = array_map('intval', $userIds);
1308 + $userIds = array_filter($userIds);
1309 +
1310 + if (empty($userIds)) return;
1311 +
1312 + acym_query('UPDATE `#__acym_user` SET `active` = 1, `deactivation_date` = NULL WHERE `id` IN ('.implode(',', $userIds).')');
1313 + }
1314 +
1315 + public function confirm(int $userId): void
1316 + {
1149 1317 $user = $this->getOneById($userId);
1150 1318 if (empty($user)) return;
1151 1319
1152 - $confirmDate = date('Y-m-d H:i:s', time() - date('Z'));
1320 + // We confirm the user and add the confirmation_date and confirmation_ip in the table.
1321 + $confirmDate = gmdate('Y-m-d H:i:s', time());
1153 1322 $ip = acym_getIP();
1154 1323 $query = 'UPDATE `#__acym_user`';
1155 1324 $query .= ' SET `confirmed` = 1, `confirmation_date` = '.acym_escapeDB($confirmDate).', `confirmation_ip` = '.acym_escapeDB($ip);
1156 1325 $query .= ' WHERE `id` = '.intval($userId).' LIMIT 1';
@@ -1161,9 +1330,10 @@
1161 1330 $res = false;
1162 1331 }
1163 1332 if ($res === false) {
1164 1333 $errorMessage = isset($e) ? $e->getMessage() : acym_getDBError();
1165 - $msg = acym_translation('ACYM_CONTACT_ADMIN_ERROR').'<br />'.substr(strip_tags($errorMessage), 0, 200).'...';
1334 + // If there is an error we definitely want to warn the user about it.
1335 + $msg = acym_translation('ACYM_CONTACT_ADMIN_ERROR').'<br />'.substr(acym_stripTags($errorMessage), 0, 200).'...';
1166 1336 acym_display($msg, 'error');
1167 1337 exit;
1168 1338 }
1169 1339
@@ -1182,9 +1352,9 @@
1182 1352 $listClass = new ListClass();
1183 1353 $listClass->sendWelcome($userId, $listIDs);
1184 1354 }
1185 1355
1186 - public function getOneByIdWithCustomFields($id): array
1356 + public function getOneByIdWithCustomFields(int $id): array
1187 1357 {
1188 1358 $user = $this->getOneById($id);
1189 1359 if (empty($user)) {
1190 1360 return [];
@@ -1206,47 +1376,11 @@
1206 1376
1207 1377 return array_merge($user, $fieldsValue);
1208 1378 }
1209 1379
1210 - public function getOneWithCustomFields($column, $value): array
1380 + public function getCustomFieldValueById(int $id): array
1211 1381 {
1212 - switch ($column) {
1213 - case 'id':
1214 - $user = $this->getOneById($value);
1215 - break;
1216 - case 'email':
1217 - $user = $this->getOneByEmail($value);
1218 - break;
1219 - case 'cms_id':
1220 - $user = $this->getOneByCMSId($value);
1221 - break;
1222 - default:
1223 - return [];
1224 - }
1225 -
1226 - if (empty($user)) {
1227 - return [];
1228 - }
1229 - $user = get_object_vars($user);
1230 -
1231 1382 $fieldsValue = acym_loadObjectList(
1232 - 'SELECT user_field.`value`, field.`name`
1233 - FROM #__acym_user_has_field AS `user_field`
1234 - LEFT JOIN #__acym_field AS `field` ON user_field.`field_id` = field.`id`
1235 - WHERE user_field.`user_id` = '.intval($user['id']),
1236 - 'name'
1237 - );
1238 -
1239 - foreach ($fieldsValue as $key => $value) {
1240 - $fieldsValue[$key] = $value->value;
1241 - }
1242 -
1243 - return array_merge($user, $fieldsValue);
1244 - }
1245 -
1246 - public function getCustomFieldValueById($id)
1247 - {
1248 - $fieldsValue = acym_loadObjectList(
1249 1383 'SELECT user_field.value AS value, field.name AS name, field.type AS type, field.value AS field_params, field.id AS id
1250 1384 FROM #__acym_user_has_field as user_field
1251 1385 LEFT JOIN #__acym_field as field ON user_field.field_id = field.id
1252 1386 WHERE user_field.user_id = '.intval($id),
@@ -1272,12 +1406,13 @@
1272 1406
1273 1407 return $fieldReturn;
1274 1408 }
1275 1409
1276 - public function getAllColumnsUserAndCustomField($inAction = false)
1410 + public function getAllColumnsUserAndCustomField(bool $inAction = false, bool $withSubInfos = false): array
1277 1411 {
1278 1412 $return = [];
1279 1413
1414 + // Basic fields in the user table
1280 1415 $userFields = acym_getColumns('user');
1281 1416 foreach ($userFields as $value) {
1282 1417 $return[$value] = $value;
1283 1418 }
@@ -1283,8 +1418,9 @@
1283 1418 }
1284 1419
1285 1420 $languageFieldId = $this->config->get(FieldClass::LANGUAGE_FIELD_ID_KEY, 0);
1286 1421
1422 + // Acy custom fields except name, email and language because they already are in the user table
1287 1423 $customFields = acym_loadObjectList(
1288 1424 'SELECT * FROM #__acym_field WHERE id NOT IN (1, 2, '.intval($languageFieldId).') '.($inAction ? 'AND type != "phone"' : ''),
1289 1425 'id'
1290 1426 );
@@ -1293,14 +1429,31 @@
1293 1429 $return[$key] = $value->name;
1294 1430 }
1295 1431 }
1296 1432
1433 + if ($withSubInfos) {
1434 + $listCLass = new ListClass();
1435 + $listFields = $listCLass->getAll('name');
1436 + if (!empty($listFields)) {
1437 + foreach ($listFields as $key => $value) {
1438 + if ($value->type === ListClass::LIST_TYPE_FOLLOWUP) {
1439 + continue;
1440 + }
1441 + $return['subscription_date_'.$value->id] = acym_translation('ACYM_SUBSCRIPTION_DATE').' '.$value->name;
1442 + $return['unsubscription_date_'.$value->id] = acym_translation('ACYM_UNSUBSCRIPTION_DATE').' '.$value->name;
1443 + }
1444 + }
1445 + }
1446 +
1297 1447 return $return;
1298 1448 }
1299 1449
1300 - public function getAllUserFields($user)
1450 + public function getAllUserFields(object $user): object
1301 1451 {
1302 - if (empty($user->id)) return $user;
1452 + if (empty($user->id)) {
1453 + return $user;
1454 + }
1455 +
1303 1456 $query = 'SELECT field.*, field.value AS field_value, userfield.*
1304 1457 FROM #__acym_field AS field
1305 1458 LEFT JOIN #__acym_user_has_field AS userfield ON field.id = userfield.field_id AND userfield.user_id = '.intval($user->id).'
1306 1459 WHERE field.id NOT IN(1, 2)';
@@ -1323,11 +1476,11 @@
1323 1476 }
1324 1477 $oneField->value = implode(',', $values);
1325 1478 }
1326 1479 } elseif ($oneField->type === 'file' && !empty($oneField->value)) {
1327 - $oneField->value = json_decode($oneField->value);
1328 - if (!empty($oneField->value)) {
1329 - $oneField->value = $oneField->value[0];
1480 + $tryJsonDecode = json_decode($oneField->value);
1481 + if (json_last_error() === JSON_ERROR_NONE && is_array($tryJsonDecode)) {
1482 + $oneField->value = !empty($tryJsonDecode) ? $tryJsonDecode[0] : null;
1330 1483 }
1331 1484 } elseif ($oneField->type === 'date') {
1332 1485 if (empty($fieldOptions->format)) {
1333 1486 acym_logError('Date field with ID '.$oneField->id.' without format option');
@@ -1344,15 +1497,16 @@
1344 1497
1345 1498 return $user;
1346 1499 }
1347 1500
1348 - public function getAllSimpleData()
1501 + public function getAllSimpleData(): array
1349 1502 {
1350 1503 return acym_loadObjectList('SELECT email, name FROM #__acym_user');
1351 1504 }
1352 1505
1353 - public function synchSaveCmsUser($user, $isnew, $oldUser = null)
1506 + public function synchSaveCmsUser(array $user, bool $isnew, ?array $oldUser = null): void
1354 1507 {
1508 + // If the source is not already defined, we define it here
1355 1509 $source = acym_getVar('string', 'acy_source', '');
1356 1510 if (empty($source)) acym_setVar('acy_source', ACYM_CMS);
1357 1511
1358 1512 if (!$this->config->get('regacy', 0)) return;
@@ -1360,21 +1514,26 @@
1360 1514 $this->checkVisitor = false;
1361 1515 $this->sendConf = false;
1362 1516
1363 1517 $regacyForceConf = $this->config->get('regacy_forceconf', 0);
1518 + /* * * * * * * * * * * * * * * * * * *
1519 + * Step 1: create / update the user *
1520 + * * * * * * * * * * * * * * * * * * */
1364 1521 $cmsUser = new \stdClass();
1365 - $cmsUser->email = trim(strip_tags($user['email']));
1522 + $cmsUser->email = trim(acym_stripTags($user['email']));
1366 1523 if (!acym_isValidEmail($cmsUser->email)) return;
1367 - if (!empty($user['name'])) $cmsUser->name = trim(strip_tags($user['name']));
1524 + if (!empty($user['name'])) $cmsUser->name = trim(acym_stripTags($user['name']));
1368 1525 if (!$regacyForceConf) $cmsUser->confirmed = 1;
1369 1526 $cmsUser->active = 1 - intval($user['block']);
1370 1527 $cmsUser->cms_id = $user['id'];
1371 1528
1372 1529 if (!$isnew && !empty($oldUser['email']) && $user['email'] != $oldUser['email']) {
1530 + // The user changed its email address, load the current Acy user if any
1373 1531 $acyUser = $this->getOneByEmail($oldUser['email']);
1374 1532 if (!empty($acyUser)) $cmsUser->id = $acyUser->id;
1375 1533 }
1376 1534
1535 + // Just in case of this is an existing user but the e-mail address has been modified by something else
1377 1536 if (empty($cmsUser->id) && !empty($cmsUser->cms_id)) {
1378 1537 $acyUser = $this->getOneByCMSId($cmsUser->cms_id);
1379 1538 if (!empty($acyUser)) $cmsUser->id = $acyUser->id;
1380 1539 }
@@ -1379,13 +1538,16 @@
1379 1538 if (!empty($acyUser)) $cmsUser->id = $acyUser->id;
1380 1539 }
1381 1540
1382 1541 $acyUser = $this->getOneByEmail($cmsUser->email);
1542 + // If an Acy user with the same email address already exists
1383 1543 if (!empty($acyUser)) {
1544 + // And wasn't linked to the site account, link it
1384 1545 if (empty($cmsUser->id)) {
1385 1546 $cmsUser->id = $acyUser->id;
1386 1547 } elseif ($cmsUser->id != $acyUser->id) {
1387 - $this->delete($acyUser->id);
1548 + // And has a different id, delete it
1549 + $this->delete([$acyUser->id]);
1388 1550 }
1389 1551 } else {
1390 1552 $cmsUser->source = $source;
1391 1553 }
@@ -1392,17 +1554,25 @@
1392 1554
1393 1555 $isnew = $isnew || empty($cmsUser->id);
1394 1556
1395 1557 $id = $this->save($cmsUser);
1558 + if (empty($id)) {
1559 + return;
1560 + }
1396 1561
1562 + // Force trigger confirmation process on cms user confirmation (send welcome emails, automation, save history...)
1397 1563 $confirmationRequired = $this->config->get('require_confirmation', 1);
1398 1564 if (!$isnew && !$regacyForceConf && $user['block'] == 0 && !empty($oldUser['block']) && $confirmationRequired == 1) {
1399 1565 $this->confirm($id);
1400 1566 }
1401 1567
1568 + /* * * * * * * * * * * * * * * * * * * * * *
1569 + * Step 2: Handle the user's subscription *
1570 + * * * * * * * * * * * * * * * * * * * * * */
1402 1571
1403 1572 $currentSubscription = $this->getSubscriptionStatus($id);
1404 1573
1574 + // In the Acy configuration, we can tell AcyMailing to automatically subscribe newly created users to some lists
1405 1575 $autoLists = $isnew ? $this->config->get('regacy_autolists', '') : '';
1406 1576 $autoLists = explode(',', $autoLists);
1407 1577 acym_arrayToInteger($autoLists);
1408 1578
@@ -1408,8 +1578,9 @@
1408 1578
1409 1579 $listsClass = new ListClass();
1410 1580 $allLists = $listsClass->getAll();
1411 1581
1582 + // The user can select some lists on the registration form
1412 1583 $visibleLists = acym_getVar('string', 'regacy_visible_lists', '');
1413 1584 $visibleLists = explode(',', $visibleLists);
1414 1585 acym_arrayToInteger($visibleLists);
1415 1586
@@ -1416,8 +1587,9 @@
1416 1587 $visibleListsChecked = acym_getVar('array', 'regacy_visible_lists_checked', []);
1417 1588 acym_arrayToInteger($visibleListsChecked);
1418 1589
1419 1590
1591 + // Handle the unsubscription
1420 1592 if (!$isnew && !empty($visibleLists)) {
1421 1593 $currentlySubscribedLists = [];
1422 1594 foreach ($currentSubscription as $oneSubscription) {
1423 1595 if ($oneSubscription->status == 1) $currentlySubscribedLists[] = $oneSubscription->list_id;
@@ -1422,31 +1594,37 @@
1422 1594 foreach ($currentSubscription as $oneSubscription) {
1423 1595 if ($oneSubscription->status == 1) $currentlySubscribedLists[] = $oneSubscription->list_id;
1424 1596 }
1425 1597 $unsubscribeLists = array_intersect($currentlySubscribedLists, array_diff($visibleLists, $visibleListsChecked));
1426 - $this->unsubscribe($id, $unsubscribeLists);
1598 + $this->unsubscribe([$id], $unsubscribeLists);
1427 1599 }
1428 1600
1601 + // Handle the subscription
1429 1602 $listsToSubscribe = [];
1430 1603 foreach ($allLists as $oneList) {
1431 1604 if (!$oneList->active) continue;
1432 1605 if (!empty($currentSubscription[$oneList->id]) && $currentSubscription[$oneList->id]->status == 1) continue;
1433 1606
1434 - if (in_array($oneList->id, $visibleListsChecked) || (in_array($oneList->id, $autoLists) && !in_array(
1435 - $oneList->id,
1436 - $visibleLists
1437 - ) && empty($currentSubscription[$oneList->id]))) {
1607 + if (
1608 + in_array($oneList->id, $visibleListsChecked)
1609 + || (
1610 + in_array($oneList->id, $autoLists)
1611 + && !in_array($oneList->id, $visibleLists) && empty($currentSubscription[$oneList->id])
1612 + )
1613 + ) {
1438 1614 $listsToSubscribe[] = $oneList->id;
1439 1615 }
1440 1616 }
1441 1617
1442 - if (!empty($listsToSubscribe)) $this->subscribe($id, $listsToSubscribe);
1618 + if (!empty($listsToSubscribe)) $this->subscribe([$id], $listsToSubscribe);
1443 1619
1444 1620 if ($isnew) $this->sendNotification($id, 'acy_notification_create');
1445 1621
1622 + // We don't force the confirmation email, or the user is disabled, or he's already confirmed
1446 1623 $acymailingUser = $this->getOneById($id);
1447 1624 if (!empty($user['block']) || !empty($acymailingUser->confirmed)) return;
1448 1625
1626 + // New active user, or just activated the user, send the email
1449 1627 if ($isnew || !empty($oldUser['block'])) {
1450 1628 if ($confirmationRequired && $regacyForceConf) {
1451 1629 $this->forceConf = true;
1452 1630 $this->sendConfirmation($id);
@@ -1451,8 +1629,9 @@
1451 1629 $this->forceConf = true;
1452 1630 $this->sendConfirmation($id);
1453 1631 }
1454 1632
1633 + // Send welcome emails on CMS user confirmation (no Acym confirmation required)
1455 1634 if (!$confirmationRequired && !empty($oldUser['email'])) {
1456 1635 $listIDs = acym_loadResultArray('SELECT `list_id` FROM `#__acym_user_has_list` WHERE `status` = 1 AND `user_id` = '.intval($id));
1457 1636 if (empty($listIDs)) return;
1458 1637 $listsClass->sendWelcome($id, $listIDs);
@@ -1459,9 +1638,9 @@
1459 1638 }
1460 1639 }
1461 1640 }
1462 1641
1463 - public function synchDeleteCmsUser($userEmail)
1642 + public function synchDeleteCmsUser(string $userEmail): void
1464 1643 {
1465 1644 $acyUser = $this->getOneByEmail($userEmail);
1466 1645
1467 1646 if (empty($acyUser)) return;
@@ -1466,22 +1645,25 @@
1466 1645
1467 1646 if (empty($acyUser)) return;
1468 1647
1469 1648 if ($this->config->get('regacy', '0') == 1 && $this->config->get('regacy_delete', '0') == 1) {
1470 - $this->delete($acyUser->id);
1649 + $this->delete([$acyUser->id]);
1471 1650 } else {
1472 1651 acym_query('UPDATE #__acym_user SET `cms_id` = 0 WHERE `id` = '.intval($acyUser->id));
1473 1652 }
1474 1653 }
1475 1654
1476 - public function getUsersLikeEmail($pattern)
1655 + /**
1656 + * Search users via partial email address
1657 + */
1658 + public function getUsersLikeEmail(string $search): array
1477 1659 {
1478 - $query = 'SELECT id, email FROM #__acym_user WHERE email LIKE '.acym_escapeDB('%'.$pattern.'%');
1660 + $query = 'SELECT id, email FROM #__acym_user WHERE email LIKE '.acym_escapeDB('%'.$search.'%');
1479 1661
1480 1662 return acym_loadObjectList($query);
1481 1663 }
1482 1664
1483 - public function sendNotification($userId, string $notification, array $params = [])
1665 + public function sendNotification(int $userId, string $notification, array $params = []): void
1484 1666 {
1485 1667 if (empty($userId) || $this->blockNotifications) return;
1486 1668
1487 1669 $notifyUsers = explode(',', $this->config->get($notification));
@@ -1491,9 +1673,9 @@
1491 1673 $mailerHelper->report = false;
1492 1674 $mailerHelper->autoAddUser = true;
1493 1675
1494 1676 $user = $this->getOneById($userId);
1495 - $userField = $this->getAllUserFields($user);
1677 + $userField = empty($user) ? null : $this->getAllUserFields($user);
1496 1678 if (!empty($userField)) {
1497 1679 foreach ($userField as $map => $value) {
1498 1680 $mailerHelper->addParam('user:'.$map, $value);
1499 1681 }
@@ -1498,8 +1680,9 @@
1498 1680 $mailerHelper->addParam('user:'.$map, $value);
1499 1681 }
1500 1682 }
1501 1683
1684 + // Load the subscription
1502 1685 $rawSubscription = $this->getUserSubscriptionById($userId);
1503 1686 $subscription = [''];
1504 1687 foreach ($rawSubscription as $listId => $listData) {
1505 1688 $currentList = $listData->name.' => ';
@@ -1523,33 +1706,44 @@
1523 1706 if (!acym_isValidEmail($oneUser)) {
1524 1707 continue;
1525 1708 }
1526 1709
1710 + $admin = $this->getOneByEmail($oneUser, true);
1711 +
1527 1712 $notificationEmail = $mailClass->getOneByName($notification);
1528 - if (!empty($notificationEmail)) {
1529 - $mailerHelper->sendOne($notificationEmail->id, $oneUser);
1713 + if (!empty($admin) && !empty($notificationEmail)) {
1714 + try {
1715 + $mailerHelper->sendOne($notificationEmail->id, $admin->id);
1716 + } catch (\Exception $e) {
1717 + acym_logError($e->getMessage());
1718 + }
1530 1719 }
1531 1720 }
1532 1721 }
1533 1722
1534 - public function getMailHistory($userId)
1723 + public function getMailHistory(int $userId)
1535 1724 {
1536 - $query = 'SELECT user_stat.*, mail.id, mail.subject, SUM(url_click.click) as click FROM #__acym_user_stat AS user_stat
1537 - JOIN #__acym_mail AS mail ON mail.id = user_stat.mail_id
1538 - LEFT JOIN #__acym_url_click AS url_click ON user_stat.mail_id = url_click.mail_id AND url_click.user_id = '.intval($userId).'
1539 - WHERE user_stat.user_id = '.intval($userId).' GROUP BY mail_id ORDER BY send_date DESC LIMIT 50';
1540 -
1541 - $mailHistory = acym_loadObjectList($query, 'mail_id');
1725 + $mailHistory = acym_loadObjectList(
1726 + 'SELECT user_stat.*, mail.id, mail.subject, SUM(url_click.click) as click
1727 + FROM #__acym_user_stat AS user_stat
1728 + JOIN #__acym_mail AS mail ON mail.id = user_stat.mail_id
1729 + LEFT JOIN #__acym_url_click AS url_click ON user_stat.mail_id = url_click.mail_id AND url_click.user_id = '.intval($userId).'
1730 + WHERE user_stat.user_id = '.intval($userId).'
1731 + GROUP BY mail_id
1732 + ORDER BY send_date DESC
1733 + LIMIT 50',
1734 + 'mail_id'
1735 + );
1542 1736 $mailClass = new MailClass();
1543 1737
1544 1738 return $mailClass->decode($mailHistory);
1545 1739 }
1546 1740
1547 - public function deleteHistoryPeriod()
1741 + public function deleteHistoryPeriod(): array
1548 1742 {
1549 - if (empty($this->config->get('delete_user_history_enabled', 0))) return;
1743 + if (empty($this->config->get('delete_user_history_enabled', 0))) return [];
1550 1744 $deleteOverSecond = $this->config->get('delete_user_history', 0);
1551 - if (empty($deleteOverSecond)) return;
1745 + if (empty($deleteOverSecond)) return [];
1552 1746 $date = time() - $deleteOverSecond;
1553 1747
1554 1748 $query = 'DELETE FROM #__acym_history WHERE date < '.intval($date);
1555 1749
@@ -1563,15 +1757,12 @@
1563 1757
1564 1758 return ['status' => $status !== false, 'message' => $message];
1565 1759 }
1566 1760
1567 - public function resetSubscription($userIds, $lists)
1761 + public function resetSubscription(array $userIds, array $lists): bool
1568 1762 {
1569 1763 if (empty($lists)) return false;
1570 1764
1571 - if (!is_array($userIds)) $userIds = [$userIds];
1572 - if (!is_array($lists)) $lists = [$lists];
1573 -
1574 1765 acym_arrayToInteger($userIds);
1575 1766 acym_arrayToInteger($lists);
1576 1767
1577 1768 foreach ($userIds as $userId) {
@@ -1595,9 +1786,9 @@
1595 1786
1596 1787 return true;
1597 1788 }
1598 1789
1599 - public function addMissingKeys()
1790 + public function addMissingKeys(): int
1600 1791 {
1601 1792 $usersMissingKey = acym_loadResultArray('SELECT `id` FROM #__acym_user WHERE `key` IS NULL OR `key` = ""');
1602 1793 foreach ($usersMissingKey as $oneUserId) {
1603 1794 acym_query('UPDATE #__acym_user SET `key` = '.acym_escapeDB(acym_generateKey(14)).' WHERE `id` = '.intval($oneUserId));
@@ -1605,9 +1796,9 @@
1605 1796
1606 1797 return count($usersMissingKey);
1607 1798 }
1608 1799
1609 - public function hasUserAccess($subscriberId): bool
1800 + public function hasUserAccess(int $subscriberId): bool
1610 1801 {
1611 1802 $userId = acym_currentUserId();
1612 1803 if (empty($userId)) {
1613 1804 return false;
@@ -1612,9 +1803,9 @@
1612 1803 if (empty($userId)) {
1613 1804 return false;
1614 1805 }
1615 1806
1616 - if (acym_isAdmin()) {
1807 + if (acym_isAdmin() && acym_isAllowed('users')) {
1617 1808 return true;
1618 1809 }
1619 1810
1620 1811 $user = $this->getOneById($subscriberId);
@@ -1636,26 +1827,25 @@
1636 1827
1637 1828 return acym_loadResult($query) > 0;
1638 1829 }
1639 1830
1640 - public function getXUsers(array $options = [])
1831 + public function getXUsers(array $options = []): array
1641 1832 {
1642 1833 $limit = $options['limit'] ?? 10;
1643 1834 $offset = $options['offset'] ?? 0;
1644 1835 $filters = $options['filters'] ?? [];
1836 + $orderBy = $options['orderBy'] ?? '';
1837 + $orderBySort = strtolower((string)($options['orderBySort'] ?? ''));
1838 + if (!in_array($orderBySort, ['asc', 'desc'], true)) {
1839 + $orderBySort = 'asc';
1840 + }
1645 1841
1646 1842 $conditions = [];
1647 1843 foreach ($filters as $column => $filter) {
1648 - switch ($column) {
1649 - case 'id':
1650 - case 'cms_id':
1651 - case 'active':
1652 - case 'confirmed':
1653 - case 'tracking':
1654 - $conditions[] = acym_secureDBColumn($column).' = '.intval($filter);
1655 - break;
1656 - default:
1657 - $conditions[] = acym_secureDBColumn($column).' LIKE '.acym_escapeDB('%'.$filter.'%');
1844 + if (in_array($column, ['id', 'cms_id', 'active', 'confirmed', 'tracking'])) {
1845 + $conditions[] = acym_secureDBColumn($column).' = '.intval($filter);
1846 + } else {
1847 + $conditions[] = acym_secureDBColumn($column).' LIKE '.acym_escapeDB('%'.$filter.'%');
1658 1848 }
1659 1849 }
1660 1850
1661 1851 if (!empty($options['created_after'])) {
@@ -1670,7 +1860,23 @@
1670 1860 if (!empty($conditions)) {
1671 1861 $query .= ' WHERE '.implode(' AND ', $conditions);
1672 1862 }
1673 1863
1864 + $orderByColumn = empty($orderBy) ? $this->pkey : acym_secureDBColumn($orderBy);
1865 + $query .= ' ORDER BY '.$orderByColumn.' '.$orderBySort;
1866 +
1867 + // Any other column can hold duplicates, we need a unique one to break the ties
1868 + if ($orderByColumn !== $this->pkey) {
1869 + $query .= ', '.$this->pkey.' ASC';
1870 + }
1871 +
1674 1872 return acym_loadObjectList($query, $this->pkey, $offset, $limit);
1873 + }
1874 +
1875 + public function disableTracking(object $user, int $mailId): void
1876 + {
1877 + acym_query('UPDATE #__acym_user SET `tracking` = 0 WHERE `id` = '.intval($user->id));
1878 +
1879 + $historyClass = new HistoryClass();
1880 + $historyClass->insert($user->id, 'tracking_disabled', [], $mailId);
1675 1881 }
1676 1882 }