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 +149 -32 10.10.2 → 11.1.0 View file →
@@ -19,9 +19,8 @@
19 19 'creation_date',
20 20 ];
21 21
22 22 private const FORM_ALLOWED_FIELDS = [
23 - 'id',
24 23 'email',
25 24 'name',
26 25 'language',
27 26 'tracking',
@@ -36,8 +35,9 @@
36 35 public bool $subscribed = false;
37 36 public string $confirmationSentError;
38 37 public bool $triggers = true;
39 38
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 40 public bool $forceConfAdmin = false;
41 41
42 42 public bool $sendWelcomeEmail = true;
43 43 public bool $sendUnsubscribeEmail = true;
@@ -52,15 +52,19 @@
52 52 $this->table = 'user';
53 53 $this->pkey = 'id';
54 54 }
55 55
56 + /**
57 + * Get users depending on filters (search, status, pagination)
58 + */
56 59 public function getMatchingElements(array $settings = []): array
57 60 {
58 61 $results = [];
62 + // Initialize the queries
59 63 $columns = '`user`.*';
60 64 if (!empty($settings['columns'])) {
61 65 foreach ($settings['columns'] as $key => $value) {
62 - $settings['columns'][$key] = $key === 'join' ? $value : 'user.'.$value;
66 + $settings['columns'][$key] = $key === 'join' ? acym_secureDBColumn($value) : 'user.'.acym_secureDBColumn($value);
63 67 }
64 68 $columns = implode(', ', $settings['columns']);
65 69 }
66 70
@@ -100,12 +104,15 @@
100 104 $pagination = new PaginationHelper();
101 105 $settings['elementsPerPage'] = $pagination->getListLimit();
102 106 }
103 107
108 + // This line warns the database that a big query is about to be ran
104 109 acym_query('SET SQL_BIG_SELECTS=1');
110 + // The beginning of $query can be modified in handleSearchFilter, don't move the SELECT up
105 111 $results['elements'] = acym_loadObjectList('SELECT DISTINCT '.$query, '', $settings['offset'], $settings['elementsPerPage']);
106 112 $results['total'] = acym_loadObject($queryCount);
107 113
114 + // Get CMS username separately if needed
108 115 if (!empty($results['elements']) && !empty($settings['cms_username'])) {
109 116 $cmsIds = array_diff(array_column($results['elements'], 'cms_id'), [0, '']);
110 117 if (!empty($cmsIds)) {
111 118 global $acymCmsUserVars;
@@ -116,8 +123,9 @@
116 123 'id'
117 124 );
118 125 foreach ($results['elements'] as $key => $oneElement) {
119 126 if (empty($results['elements'][$key]->cms_id)) continue;
127 + // Clean cms link on subscribers where CMS user no longer exists
120 128 if (empty($userNames[$results['elements'][$key]->cms_id])) {
121 129 $oneUser = $this->getOneById($results['elements'][$key]->id);
122 130 $oneUser->cms_id = 0;
123 131 $this->save($oneUser);
@@ -180,8 +188,9 @@
180 188
181 189 $searchValue = acym_escapeDB('%'.$settings['search'].'%');
182 190 $searchFilter = 'user.email LIKE '.$searchValue.' OR user.name LIKE '.$searchValue.' OR user.id = '.intval($settings['search']);
183 191
192 + // Search in visible custom fields
184 193 $listingFields = acym_loadResultArray('SELECT `id` FROM #__acym_field WHERE `'.(acym_isAdmin() ? 'back' : 'front').'end_listing` = 1');
185 194 if (!empty($listingFields)) {
186 195 $query = 'DISTINCT '.$query;
187 196 $join = ' LEFT JOIN #__acym_user_has_field AS userfield ON user.id = userfield.user_id AND userfield.field_id IN ('.implode(', ', $listingFields).') AND ';
@@ -270,8 +279,9 @@
270 279 $filters[] = 'user.id IN('.implode(',', $settings['onlyElements']).')';
271 280 }
272 281 }
273 282
283 + // For the entity select when selecting "show only selected users"
274 284 if (!empty($settings['showOnlySelected'])) {
275 285 if (!empty($settings['selectedUsers'])) {
276 286 acym_arrayToInteger($settings['selectedUsers']);
277 287 $filters[] = 'user.id IN('.implode(',', $settings['selectedUsers']).')';
@@ -302,20 +312,23 @@
302 312 }
303 313 }
304 314 }
305 315
306 - $where = '';
316 + $whereBlocks = [];
307 317 $join = '';
308 - foreach ($automationHelpers as $index => $automationHelper) {
318 + foreach ($automationHelpers as $automationHelper) {
309 319 if (!empty($automationHelper->where)) {
310 - $where .= ' ('.implode(') and (', $automationHelper->where).')';
311 - if ($index != count($automationHelpers) - 1) $where .= ' OR ';
320 + $whereBlocks[] = ' ('.implode(') and (', $automationHelper->where).')';
312 321 }
313 - if (!empty($automationHelper->join)) $join .= ' JOIN '.implode(' JOIN ', $automationHelper->join);
314 - 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 + }
315 328 }
316 - if (!empty($where)) {
317 - $filters[] = $where;
329 + if (!empty($whereBlocks)) {
330 + $filters[] = implode(' OR ', $whereBlocks);
318 331 }
319 332 $query .= $join;
320 333 $queryCount .= $join;
321 334 $queryStatus .= $join;
@@ -458,8 +471,9 @@
458 471 }
459 472
460 473 public function getUsersSubscriptionsByIds(array $userIds, int $creatorId = 0): array
461 474 {
475 + acym_arrayToInteger($userIds);
462 476 $query = 'SELECT `list`.`id`, `list`.`color`, `list`.`name`, `userlist`.`user_id`, `userlist`.`status`
463 477 FROM #__acym_list AS `list`
464 478 JOIN #__acym_user_has_list AS `userlist`
465 479 ON `list`.`id` = `userlist`.`list_id`
@@ -467,10 +481,14 @@
467 481 AND `list`.`type` = '.acym_escapeDB(ListClass::LIST_TYPE_STANDARD);
468 482
469 483 if (!empty($creatorId)) {
470 484 $userGroups = acym_getGroupsByUser($creatorId);
471 - $groupCondition = '`list`.`access` LIKE "%,'.implode(',%" OR `list`.`access` LIKE "%,', $userGroups).',%"';
472 - $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).')';
473 491 }
474 492 $query .= ' ORDER BY `userlist`.`status` DESC, `list`.`id` ASC';
475 493
476 494 return acym_loadObjectList($query);
@@ -499,8 +517,15 @@
499 517
500 518 return acym_loadObjectList($query, 'list_id');
501 519 }
502 520
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 + */
503 528 public function identify(bool $onlyValue = false, ?string $idName = null, ?string $keyName = null): ?object
504 529 {
505 530 $id = acym_getVar('int', empty($idName) ? 'id' : $idName, 0);
506 531 $key = acym_getVar('string', empty($keyName) ? 'key' : $keyName, '');
@@ -510,8 +535,10 @@
510 535 $key = acym_getVar('string', 'key', 0);
511 536 }
512 537
513 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...
514 541 $currentUserid = acym_currentUserId();
515 542 if (!empty($currentUserid)) {
516 543 return $this->getOneByCMSId($currentUserid);
517 544 }
@@ -539,12 +566,15 @@
539 566 {
540 567 if (empty($addLists)) {
541 568 return false;
542 569 }
570 + acym_arrayToInteger($addLists);
543 571
572 + //TODO was previously shown as an int in the dev documentation, hence not adding the type yet
544 573 if (!is_array($userIds)) {
545 574 $userIds = [$userIds];
546 575 }
576 + acym_arrayToInteger($userIds);
547 577
548 578 $listClass = new ListClass();
549 579 $historyClass = new HistoryClass();
550 580
@@ -561,10 +591,9 @@
561 591 $currentlyUnsubscribed = [];
562 592 foreach ($currentSubscription as $oneList) {
563 593 if ($oneList->status == 1) {
564 594 $currentlySubscribed[$oneList->id] = $oneList;
565 - }
566 - if ($oneList->status == 0) {
595 + } else {
567 596 $currentlyUnsubscribed[$oneList->id] = $oneList;
568 597 }
569 598 }
570 599
@@ -570,8 +599,9 @@
570 599
571 600 $subscribedLists = [];
572 601 $listsNotExisting = [];
573 602 foreach ($addLists as $oneListId) {
603 + // The user is already subscribed
574 604 if (empty($oneListId) || !empty($currentlySubscribed[$oneListId])) {
575 605 continue;
576 606 }
577 607
@@ -584,13 +614,15 @@
584 614 $subscription = new \stdClass();
585 615 $subscription->user_id = $userId;
586 616 $subscription->list_id = $oneListId;
587 617 $subscription->status = 1;
588 - $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());
589 619
590 620 if (empty($currentSubscription[$oneListId])) {
621 + // The user isn't already subscribed, we subscribe him!
591 622 acym_insertObject('#__acym_user_has_list', $subscription);
592 623 } elseif (!empty($currentlyUnsubscribed[$oneListId])) {
624 + // The user re-subscribes or we have to confirm the subscription
593 625 acym_updateObject('#__acym_user_has_list', $subscription, ['user_id', 'list_id']);
594 626 }
595 627
596 628 $subscribedLists[] = $oneListId;
@@ -626,8 +658,9 @@
626 658 $countUnsubscribe = 0;
627 659
628 660 foreach ($userIds as $id) {
629 661 $userStat = $userStatClass->getOneByMailAndUserId($mailId, $id);
662 + // We didn't send this email to this user, something is wrong
630 663 if (empty($userStat)) continue;
631 664
632 665 $newUserStat = new \stdClass();
633 666 $newUserStat->mail_id = $mailId;
@@ -653,11 +686,14 @@
653 686 if (empty($lists)) {
654 687 return false;
655 688 }
656 689
690 + //TODO was previously shown as an int in the dev documentation, hence not adding the type yet
657 691 if (!is_array($userIds)) {
658 692 $userIds = [$userIds];
659 693 }
694 + acym_arrayToInteger($userIds);
695 + acym_arrayToInteger($lists);
660 696
661 697 $listClass = new ListClass();
662 698 $unsubscribedFromLists = false;
663 699 foreach ($userIds as $userId) {
@@ -681,12 +717,14 @@
681 717
682 718 $unsubscribedLists = [];
683 719 $listsNotExisting = [];
684 720 foreach ($lists as $oneListId) {
721 + // The user is already unsubscribed
685 722 if (empty($oneListId) || !empty($currentlyUnsubscribed[$oneListId])) {
686 723 continue;
687 724 }
688 725
726 + // Don't unsubscribe from non-subscribed lists
689 727 if (empty($currentlySubscribed[$oneListId])) {
690 728 continue;
691 729 }
692 730
@@ -699,12 +737,14 @@
699 737 $subscription = new \stdClass();
700 738 $subscription->user_id = $userId;
701 739 $subscription->list_id = $oneListId;
702 740 $subscription->status = 0;
703 - $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());
704 742 if (empty($currentSubscription[$oneListId])) {
743 + // The user isn't already subscribed, we unsubscribe him directly
705 744 acym_insertObject('#__acym_user_has_list', $subscription);
706 745 } else {
746 + // The user unsubscribed from a list
707 747 acym_updateObject('#__acym_user_has_list', $subscription, ['user_id', 'list_id']);
708 748 }
709 749
710 750 $unsubscribedLists[] = $oneListId;
@@ -770,8 +810,9 @@
770 810 if (empty($listIds) || empty($userIds)) {
771 811 return;
772 812 }
773 813
814 + acym_arrayToInteger($userIds);
774 815 acym_arrayToInteger($listIds);
775 816
776 817 acym_query(
777 818 'DELETE FROM #__acym_user_has_list
@@ -781,9 +822,9 @@
781 822 }
782 823
783 824 public function onlyManageableUsers(array &$elements): void
784 825 {
785 - if (acym_isAdmin()) {
826 + if (acym_isAdmin() && acym_isAllowed('users')) {
786 827 return;
787 828 }
788 829
789 830 $listClass = new ListClass();
@@ -881,9 +922,14 @@
881 922 if (!isset($user->active)) {
882 923 $user->active = 1;
883 924 }
884 925
926 + if (!isset($user->tracking)) {
927 + $user->tracking = $this->config->get('dont_track_by_default', 0) ? 0 : 1;
928 + }
929 +
885 930 if (empty($user->language)) {
931 + // Take the user account's language
886 932 if (!acym_isAdmin()) {
887 933 $cmsUserLanguage = acym_getCmsUserLanguage();
888 934 }
889 935
@@ -888,11 +934,10 @@
888 934 }
889 935
890 936 if (!empty($cmsUserLanguage)) {
891 937 $user->language = $cmsUserLanguage;
892 - } elseif (acym_isMultilingual()) {
893 - $configUserLanguage = $this->config->get('multilingual_user_default', 'current_language');
894 - $user->language = $configUserLanguage === 'current_language' ? acym_getLanguageTag() : $configUserLanguage;
938 + } else {
939 + $user->language = acym_getDefaultUserLanguage();
895 940 }
896 941 }
897 942
898 943 $currentUserid = acym_currentUserId();
@@ -898,8 +943,9 @@
898 943 $currentUserid = acym_currentUserId();
899 944 $currentEmail = acym_currentUserEmail();
900 945 $allowVisitors = $this->config->get('allow_visitor', 1) == 1;
901 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
902 948 $this->errors[] = acym_translation('ACYM_ONLY_LOGGED');
903 949
904 950 return null;
905 951 }
@@ -914,13 +960,13 @@
914 960 }
915 961
916 962 $user->key = acym_generateKey(14);
917 963
918 - $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());
919 965 } elseif (!empty($user->confirmed)) {
920 966 $oldUser = $this->getOneByIdWithCustomFields($user->id);
921 967 if (!empty($oldUser) && empty($oldUser['confirmed'])) {
922 - $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());
923 969 $user->confirmation_ip = acym_getIP();
924 970
925 971 if ($this->triggers) {
926 972 acym_trigger('onAcymAfterUserConfirm', [&$user]);
@@ -946,11 +992,13 @@
946 992 }
947 993
948 994 $oneAttribute = trim(strtolower($oneAttribute));
949 995 if (!in_array($oneAttribute, self::RESTRICTED_FIELDS)) {
950 - $user->$oneAttribute = strip_tags($value);
996 + $user->$oneAttribute = acym_stripTags($value);
951 997 }
952 998
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
953 1001 if (is_numeric($user->$oneAttribute)) {
954 1002 continue;
955 1003 }
956 1004
@@ -967,8 +1015,9 @@
967 1015 $user->$oneAttribute = acym_utf8Encode($user->$oneAttribute);
968 1016 }
969 1017 }
970 1018
1019 + // Prevent SQL error on flags that aren't cleaned
971 1020 if (!empty($user->automation) && strlen($user->automation) > 20) {
972 1021 $user->automation = '';
973 1022 }
974 1023
@@ -984,9 +1033,9 @@
984 1033 if (!empty($userCmsID)) $user->cms_id = $userCmsID;
985 1034 }
986 1035 if ($this->triggers) {
987 1036 $result = acym_trigger('onAcymBeforeUserCreate', [&$user]);
988 - if (in_array(false, $result)) {
1037 + if (is_array($result) && in_array(false, $result)) {
989 1038 $acycheckerError = acym_getVar('string', 'acychecker_error');
990 1039 if (!empty($acycheckerError)) {
991 1040 if ($ajax) {
992 1041 $this->errors[] = $acycheckerError;
@@ -1005,8 +1054,9 @@
1005 1054 }
1006 1055
1007 1056 $userID = parent::save($user);
1008 1057
1058 + // Save custom fields if there are any
1009 1059 $fieldClass = new FieldClass();
1010 1060 if (!empty($customFields)) {
1011 1061 foreach ($customFields as $key => $value) {
1012 1062 if (!is_string($value)) {
@@ -1012,8 +1062,9 @@
1012 1062 if (!is_string($value)) {
1013 1063 continue;
1014 1064 }
1015 1065
1066 + // Remove most of the existing emojis that are not supported
1016 1067 $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $value);
1017 1068
1018 1069 $field = $fieldClass->getOneById($key);
1019 1070 if (empty($field) || $field->type !== 'textarea') {
@@ -1065,8 +1116,12 @@
1065 1116 $user->$attribute = $value;
1066 1117 }
1067 1118 }
1068 1119
1120 + if (!empty($connectedUser->id)) {
1121 + $user->id = $connectedUser->id;
1122 + }
1123 +
1069 1124 if (empty($user->email)) {
1070 1125 if (!empty($connectedUser->email)) {
1071 1126 $user->email = $connectedUser->email;
1072 1127 }
@@ -1088,9 +1143,11 @@
1088 1143 $allowUserModifications = true;
1089 1144 $allowSubscriptionModifications = true;
1090 1145 }
1091 1146
1147 + // Check if it already exists...
1092 1148 if (!empty($user->email)) {
1149 + // Do we already have a user with this same e-mail address?
1093 1150 if (empty($user->id)) {
1094 1151 $user->id = 0;
1095 1152 }
1096 1153 $existUser = acym_loadObject('SELECT * FROM #__acym_user WHERE email = '.acym_escapeDB($user->email).' AND id != '.intval($user->id));
@@ -1104,8 +1161,10 @@
1104 1161 }
1105 1162 }
1106 1163 }
1107 1164
1165 + // Did the user ask for its e-mail address to be modified?
1166 + // If so, we set it as not confirmed automatically.
1108 1167 if (!empty($user->id) && !empty($user->email)) {
1109 1168 $existUser = $this->getOneById($user->id);
1110 1169 if (trim(strtolower($user->email)) != strtolower($existUser->email)) {
1111 1170 $user->confirmed = 0;
@@ -1111,18 +1170,27 @@
1111 1170 $user->confirmed = 0;
1112 1171 }
1113 1172 }
1114 1173
1174 + $fromProfile = acym_getVar('int', 'acyprofile', 0) == 1;
1175 +
1115 1176 $this->newUser = empty($user->id);
1116 1177 if (empty($user->id) || $allowUserModifications) {
1117 1178 if ($this->newUser && $this->config->get('require_confirmation', 1) == 1) {
1118 1179 $user->confirmed = 0;
1119 1180 }
1181 +
1182 + if ($fromProfile && !(isset($user->confirmed) && empty($user->confirmed))) {
1183 + $this->sendConf = false;
1184 + }
1185 +
1186 + // Get custom fields to save them if exist
1120 1187 $customFieldData = acym_getVar('array', 'customField', []);
1121 1188 $id = $this->save($user, $customFieldData, $ajax);
1122 1189 $allowSubscriptionModifications = true;
1123 1190 } else {
1124 1191 $id = $user->id;
1192 + //We didn't save the user... but we should check the confirm field anyway
1125 1193 if (isset($user->confirmed) && empty($user->confirmed)) {
1126 1194 $this->sendConfirmation($id);
1127 1195 }
1128 1196 }
@@ -1134,11 +1202,12 @@
1134 1202
1135 1203 if (!acym_isAdmin()) {
1136 1204 $hiddenlistsString = acym_getVar('string', 'hiddenlists', '');
1137 1205 $hiddenlists = explode(',', $hiddenlistsString);
1138 - acym_arrayToInteger($hiddenlists);
1139 1206 $visibleSubscription = acym_getVar('array', 'subscription', []);
1140 1207 $subscribeLists = array_merge($hiddenlists, $visibleSubscription);
1208 + acym_arrayToInteger($subscribeLists);
1209 +
1141 1210 if (!empty($subscribeLists)) {
1142 1211 foreach ($subscribeLists as $oneListId) {
1143 1212 $formData['listsub'][$oneListId] = ['status' => 1];
1144 1213 }
@@ -1144,10 +1213,8 @@
1144 1213 }
1145 1214 }
1146 1215 }
1147 1216
1148 - $fromProfile = acym_getVar('int', 'acyprofile', 0) == 1;
1149 -
1150 1217 if (empty($formData['listsub'])) {
1151 1218 if (!$fromProfile) $this->sendNotification($id, 'acy_notification_subform');
1152 1219
1153 1220 $this->sendNotification(
@@ -1207,8 +1274,9 @@
1207 1274
1208 1275 $mailerHelper = new MailerHelper();
1209 1276 $mailerHelper->report = (bool)$this->config->get('confirm_message', 0);
1210 1277
1278 + //TODO $mailerHelper->addParam('user:subscription', implode('<br/>', $subscription));
1211 1279 $mailClass = new MailClass();
1212 1280 $confirmationEmail = $mailClass->getOneByName('acy_confirm');
1213 1281 $this->confirmationSentSuccess = !empty($confirmationEmail) && $mailerHelper->sendOne($confirmationEmail->id, $myUser->id);
1214 1282 $this->confirmationSentError = $mailerHelper->reportMessage;
@@ -1248,9 +1316,10 @@
1248 1316 {
1249 1317 $user = $this->getOneById($userId);
1250 1318 if (empty($user)) return;
1251 1319
1252 - $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());
1253 1322 $ip = acym_getIP();
1254 1323 $query = 'UPDATE `#__acym_user`';
1255 1324 $query .= ' SET `confirmed` = 1, `confirmation_date` = '.acym_escapeDB($confirmDate).', `confirmation_ip` = '.acym_escapeDB($ip);
1256 1325 $query .= ' WHERE `id` = '.intval($userId).' LIMIT 1';
@@ -1261,9 +1330,10 @@
1261 1330 $res = false;
1262 1331 }
1263 1332 if ($res === false) {
1264 1333 $errorMessage = isset($e) ? $e->getMessage() : acym_getDBError();
1265 - $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).'...';
1266 1336 acym_display($msg, 'error');
1267 1337 exit;
1268 1338 }
1269 1339
@@ -1340,8 +1410,9 @@
1340 1410 public function getAllColumnsUserAndCustomField(bool $inAction = false, bool $withSubInfos = false): array
1341 1411 {
1342 1412 $return = [];
1343 1413
1414 + // Basic fields in the user table
1344 1415 $userFields = acym_getColumns('user');
1345 1416 foreach ($userFields as $value) {
1346 1417 $return[$value] = $value;
1347 1418 }
@@ -1347,8 +1418,9 @@
1347 1418 }
1348 1419
1349 1420 $languageFieldId = $this->config->get(FieldClass::LANGUAGE_FIELD_ID_KEY, 0);
1350 1421
1422 + // Acy custom fields except name, email and language because they already are in the user table
1351 1423 $customFields = acym_loadObjectList(
1352 1424 'SELECT * FROM #__acym_field WHERE id NOT IN (1, 2, '.intval($languageFieldId).') '.($inAction ? 'AND type != "phone"' : ''),
1353 1425 'id'
1354 1426 );
@@ -1432,8 +1504,9 @@
1432 1504 }
1433 1505
1434 1506 public function synchSaveCmsUser(array $user, bool $isnew, ?array $oldUser = null): void
1435 1507 {
1508 + // If the source is not already defined, we define it here
1436 1509 $source = acym_getVar('string', 'acy_source', '');
1437 1510 if (empty($source)) acym_setVar('acy_source', ACYM_CMS);
1438 1511
1439 1512 if (!$this->config->get('regacy', 0)) return;
@@ -1441,21 +1514,26 @@
1441 1514 $this->checkVisitor = false;
1442 1515 $this->sendConf = false;
1443 1516
1444 1517 $regacyForceConf = $this->config->get('regacy_forceconf', 0);
1518 + /* * * * * * * * * * * * * * * * * * *
1519 + * Step 1: create / update the user *
1520 + * * * * * * * * * * * * * * * * * * */
1445 1521 $cmsUser = new \stdClass();
1446 - $cmsUser->email = trim(strip_tags($user['email']));
1522 + $cmsUser->email = trim(acym_stripTags($user['email']));
1447 1523 if (!acym_isValidEmail($cmsUser->email)) return;
1448 - if (!empty($user['name'])) $cmsUser->name = trim(strip_tags($user['name']));
1524 + if (!empty($user['name'])) $cmsUser->name = trim(acym_stripTags($user['name']));
1449 1525 if (!$regacyForceConf) $cmsUser->confirmed = 1;
1450 1526 $cmsUser->active = 1 - intval($user['block']);
1451 1527 $cmsUser->cms_id = $user['id'];
1452 1528
1453 1529 if (!$isnew && !empty($oldUser['email']) && $user['email'] != $oldUser['email']) {
1530 + // The user changed its email address, load the current Acy user if any
1454 1531 $acyUser = $this->getOneByEmail($oldUser['email']);
1455 1532 if (!empty($acyUser)) $cmsUser->id = $acyUser->id;
1456 1533 }
1457 1534
1535 + // Just in case of this is an existing user but the e-mail address has been modified by something else
1458 1536 if (empty($cmsUser->id) && !empty($cmsUser->cms_id)) {
1459 1537 $acyUser = $this->getOneByCMSId($cmsUser->cms_id);
1460 1538 if (!empty($acyUser)) $cmsUser->id = $acyUser->id;
1461 1539 }
@@ -1460,12 +1538,15 @@
1460 1538 if (!empty($acyUser)) $cmsUser->id = $acyUser->id;
1461 1539 }
1462 1540
1463 1541 $acyUser = $this->getOneByEmail($cmsUser->email);
1542 + // If an Acy user with the same email address already exists
1464 1543 if (!empty($acyUser)) {
1544 + // And wasn't linked to the site account, link it
1465 1545 if (empty($cmsUser->id)) {
1466 1546 $cmsUser->id = $acyUser->id;
1467 1547 } elseif ($cmsUser->id != $acyUser->id) {
1548 + // And has a different id, delete it
1468 1549 $this->delete([$acyUser->id]);
1469 1550 }
1470 1551 } else {
1471 1552 $cmsUser->source = $source;
@@ -1477,16 +1558,21 @@
1477 1558 if (empty($id)) {
1478 1559 return;
1479 1560 }
1480 1561
1562 + // Force trigger confirmation process on cms user confirmation (send welcome emails, automation, save history...)
1481 1563 $confirmationRequired = $this->config->get('require_confirmation', 1);
1482 1564 if (!$isnew && !$regacyForceConf && $user['block'] == 0 && !empty($oldUser['block']) && $confirmationRequired == 1) {
1483 1565 $this->confirm($id);
1484 1566 }
1485 1567
1568 + /* * * * * * * * * * * * * * * * * * * * * *
1569 + * Step 2: Handle the user's subscription *
1570 + * * * * * * * * * * * * * * * * * * * * * */
1486 1571
1487 1572 $currentSubscription = $this->getSubscriptionStatus($id);
1488 1573
1574 + // In the Acy configuration, we can tell AcyMailing to automatically subscribe newly created users to some lists
1489 1575 $autoLists = $isnew ? $this->config->get('regacy_autolists', '') : '';
1490 1576 $autoLists = explode(',', $autoLists);
1491 1577 acym_arrayToInteger($autoLists);
1492 1578
@@ -1492,8 +1578,9 @@
1492 1578
1493 1579 $listsClass = new ListClass();
1494 1580 $allLists = $listsClass->getAll();
1495 1581
1582 + // The user can select some lists on the registration form
1496 1583 $visibleLists = acym_getVar('string', 'regacy_visible_lists', '');
1497 1584 $visibleLists = explode(',', $visibleLists);
1498 1585 acym_arrayToInteger($visibleLists);
1499 1586
@@ -1500,8 +1587,9 @@
1500 1587 $visibleListsChecked = acym_getVar('array', 'regacy_visible_lists_checked', []);
1501 1588 acym_arrayToInteger($visibleListsChecked);
1502 1589
1503 1590
1591 + // Handle the unsubscription
1504 1592 if (!$isnew && !empty($visibleLists)) {
1505 1593 $currentlySubscribedLists = [];
1506 1594 foreach ($currentSubscription as $oneSubscription) {
1507 1595 if ($oneSubscription->status == 1) $currentlySubscribedLists[] = $oneSubscription->list_id;
@@ -1509,8 +1597,9 @@
1509 1597 $unsubscribeLists = array_intersect($currentlySubscribedLists, array_diff($visibleLists, $visibleListsChecked));
1510 1598 $this->unsubscribe([$id], $unsubscribeLists);
1511 1599 }
1512 1600
1601 + // Handle the subscription
1513 1602 $listsToSubscribe = [];
1514 1603 foreach ($allLists as $oneList) {
1515 1604 if (!$oneList->active) continue;
1516 1605 if (!empty($currentSubscription[$oneList->id]) && $currentSubscription[$oneList->id]->status == 1) continue;
@@ -1529,11 +1618,13 @@
1529 1618 if (!empty($listsToSubscribe)) $this->subscribe([$id], $listsToSubscribe);
1530 1619
1531 1620 if ($isnew) $this->sendNotification($id, 'acy_notification_create');
1532 1621
1622 + // We don't force the confirmation email, or the user is disabled, or he's already confirmed
1533 1623 $acymailingUser = $this->getOneById($id);
1534 1624 if (!empty($user['block']) || !empty($acymailingUser->confirmed)) return;
1535 1625
1626 + // New active user, or just activated the user, send the email
1536 1627 if ($isnew || !empty($oldUser['block'])) {
1537 1628 if ($confirmationRequired && $regacyForceConf) {
1538 1629 $this->forceConf = true;
1539 1630 $this->sendConfirmation($id);
@@ -1538,8 +1629,9 @@
1538 1629 $this->forceConf = true;
1539 1630 $this->sendConfirmation($id);
1540 1631 }
1541 1632
1633 + // Send welcome emails on CMS user confirmation (no Acym confirmation required)
1542 1634 if (!$confirmationRequired && !empty($oldUser['email'])) {
1543 1635 $listIDs = acym_loadResultArray('SELECT `list_id` FROM `#__acym_user_has_list` WHERE `status` = 1 AND `user_id` = '.intval($id));
1544 1636 if (empty($listIDs)) return;
1545 1637 $listsClass->sendWelcome($id, $listIDs);
@@ -1559,8 +1651,11 @@
1559 1651 acym_query('UPDATE #__acym_user SET `cms_id` = 0 WHERE `id` = '.intval($acyUser->id));
1560 1652 }
1561 1653 }
1562 1654
1655 + /**
1656 + * Search users via partial email address
1657 + */
1563 1658 public function getUsersLikeEmail(string $search): array
1564 1659 {
1565 1660 $query = 'SELECT id, email FROM #__acym_user WHERE email LIKE '.acym_escapeDB('%'.$search.'%');
1566 1661
@@ -1585,8 +1680,9 @@
1585 1680 $mailerHelper->addParam('user:'.$map, $value);
1586 1681 }
1587 1682 }
1588 1683
1684 + // Load the subscription
1589 1685 $rawSubscription = $this->getUserSubscriptionById($userId);
1590 1686 $subscription = [''];
1591 1687 foreach ($rawSubscription as $listId => $listData) {
1592 1688 $currentList = $listData->name.' => ';
@@ -1707,9 +1803,9 @@
1707 1803 if (empty($userId)) {
1708 1804 return false;
1709 1805 }
1710 1806
1711 - if (acym_isAdmin()) {
1807 + if (acym_isAdmin() && acym_isAllowed('users')) {
1712 1808 return true;
1713 1809 }
1714 1810
1715 1811 $user = $this->getOneById($subscriberId);
@@ -1736,8 +1832,13 @@
1736 1832 {
1737 1833 $limit = $options['limit'] ?? 10;
1738 1834 $offset = $options['offset'] ?? 0;
1739 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 + }
1740 1841
1741 1842 $conditions = [];
1742 1843 foreach ($filters as $column => $filter) {
1743 1844 if (in_array($column, ['id', 'cms_id', 'active', 'confirmed', 'tracking'])) {
@@ -1759,7 +1860,23 @@
1759 1860 if (!empty($conditions)) {
1760 1861 $query .= ' WHERE '.implode(' AND ', $conditions);
1761 1862 }
1762 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 +
1763 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);
1764 1881 }
1765 1882 }