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
acymailing / back / Classes / UserClass.php

UserClass.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 11.1.0, at back/Classes/UserClass.php

1,883 lines 71.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace AcyMailing\Classes;
4
5 use AcyMailing\Helpers\AutomationHelper;
6 use AcyMailing\Helpers\MailerHelper;
7 use AcyMailing\Helpers\PaginationHelper;
8 use AcyMailing\Core\AcymClass;
9
10 class UserClass extends AcymClass
11 {
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;
32 public bool $confirmationSentSuccess = false;
33 public bool $newUser = false;
34 public bool $blockNotifications = false;
35 public bool $subscribed = false;
36 public string $confirmationSentError;
37 public bool $triggers = true;
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 public bool $forceConfAdmin = false;
41
42 public bool $sendWelcomeEmail = true;
43 public bool $sendUnsubscribeEmail = true;
44
45 private bool $allowModif = false;
46 private bool $forceConf = false;
47
48 public function __construct()
49 {
50 parent::__construct();
51
52 $this->table = 'user';
53 $this->pkey = 'id';
54 }
55
56 /**
57 * Get users depending on filters (search, status, pagination)
58 */
59 public function getMatchingElements(array $settings = []): array
60 {
61 $results = [];
62 // Initialize the queries
63 $columns = '`user`.*';
64 if (!empty($settings['columns'])) {
65 foreach ($settings['columns'] as $key => $value) {
66 $settings['columns'][$key] = $key === 'join' ? acym_secureDBColumn($value) : 'user.'.acym_secureDBColumn($value);
67 }
68 $columns = implode(', ', $settings['columns']);
69 }
70
71 $query = $columns.' FROM #__acym_user AS `user`';
72 $queryCount = 'SELECT COUNT(DISTINCT user.id) AS total FROM #__acym_user AS user';
73 $queryStatus = 'SELECT COUNT(DISTINCT user.id) AS number, user.confirmed + user.active*2 AS score FROM #__acym_user AS user';
74 $filters = [];
75
76 if (!empty($settings['join'])) {
77 $query .= $this->getJoinForQuery($settings['join']);
78 }
79 $this->handleSegmentFilter($settings, $filters, $query, $queryCount, $queryStatus);
80 $this->handleSubscriptionFilter($settings, $filters, $query, $queryCount, $queryStatus);
81 $this->handleFrontend($settings, $query, $queryCount, $queryStatus);
82 $this->handleSearchFilter($settings, $filters, $query, $queryCount, $queryStatus);
83 $results['status'] = $this->handleUserStatusFilter($settings, $filters, $queryStatus);
84 $entityResult = $this->handleEntitySelect($settings, $filters);
85
86 if (!empty($entityResult)) {
87 return $entityResult;
88 }
89
90 if (!empty($filters)) {
91 $query .= ' WHERE ('.implode(') AND (', $filters).')';
92 $queryCount .= ' WHERE ('.implode(') AND (', $filters).')';
93 }
94
95 if (!empty($settings['ordering']) && !empty($settings['ordering_sort_order'])) {
96 $query .= ' ORDER BY user.'.acym_secureDBColumn($settings['ordering']).' '.acym_secureDBColumn(strtoupper($settings['ordering_sort_order']));
97 }
98
99 if (empty($settings['offset']) || $settings['offset'] < 0) {
100 $settings['offset'] = 0;
101 }
102
103 if (empty($settings['elementsPerPage']) || $settings['elementsPerPage'] < 1) {
104 $pagination = new PaginationHelper();
105 $settings['elementsPerPage'] = $pagination->getListLimit();
106 }
107
108 // This line warns the database that a big query is about to be ran
109 acym_query('SET SQL_BIG_SELECTS=1');
110 // The beginning of $query can be modified in handleSearchFilter, don't move the SELECT up
111 $results['elements'] = acym_loadObjectList('SELECT DISTINCT '.$query, '', $settings['offset'], $settings['elementsPerPage']);
112 $results['total'] = acym_loadObject($queryCount);
113
114 // Get CMS username separately if needed
115 if (!empty($results['elements']) && !empty($settings['cms_username'])) {
116 $cmsIds = array_diff(array_column($results['elements'], 'cms_id'), [0, '']);
117 if (!empty($cmsIds)) {
118 global $acymCmsUserVars;
119 $userNames = acym_loadObjectList(
120 'SELECT '.$acymCmsUserVars->id.' AS id, '.$acymCmsUserVars->username.' AS `cms_username`
121 FROM '.$acymCmsUserVars->table.'
122 WHERE id IN ('.implode(', ', array_values($cmsIds)).')',
123 'id'
124 );
125 foreach ($results['elements'] as $key => $oneElement) {
126 if (empty($results['elements'][$key]->cms_id)) continue;
127 // Clean cms link on subscribers where CMS user no longer exists
128 if (empty($userNames[$results['elements'][$key]->cms_id])) {
129 $oneUser = $this->getOneById($results['elements'][$key]->id);
130 $oneUser->cms_id = 0;
131 $this->save($oneUser);
132 $results['elements'][$key]->cms_id = 0;
133 continue;
134 }
135 $results['elements'][$key]->cms_username = $userNames[$results['elements'][$key]->cms_id]->cms_username;
136 }
137 }
138 }
139
140 return $results;
141 }
142
143 private function handleSubscriptionFilter(array $settings, array &$filters, string &$query, string &$queryCount, string &$queryStatus): void
144 {
145 if (empty($settings['list'])) {
146 return;
147 }
148
149 $listJoin = ' JOIN #__acym_user_has_list AS list ON user.id = list.user_id AND list.list_id = '.intval($settings['list']).' ';
150
151 if ($settings['list_status'] === 'none') {
152 $listJoin = ' LEFT'.$listJoin;
153 $filters[] = 'list.user_id IS NULL';
154 } else {
155 $filters[] = 'list.status = '.($settings['list_status'] === 'sub' ? 1 : 0);
156 }
157
158 $query .= $listJoin;
159 $queryCount .= $listJoin;
160 $queryStatus .= $listJoin;
161 }
162
163 private function handleFrontend(array $settings, string &$query, string &$queryCount, string &$queryStatus): void
164 {
165 if (!acym_isAdmin()) {
166 $settings['creator_id'] = acym_currentUserId();
167 if (empty($settings['creator_id'])) {
168 $settings['creator_id'] = '-1';
169 }
170 }
171
172 if (!empty($settings['creator_id'])) {
173 $userGroups = acym_getGroupsByUser($settings['creator_id']);
174 $groupCondition = 'list.access LIKE "%,'.implode(',%" OR list.access LIKE "%,', $userGroups).',%"';
175 $joinList = ' JOIN #__acym_user_has_list as user_list ON user_list.user_id = user.id JOIN #__acym_list AS list ON list.id = user_list.list_id AND (list.cms_user_id = '.intval(
176 $settings['creator_id']
177 ).' OR '.$groupCondition.')';
178
179 $query .= $joinList;
180 $queryCount .= $joinList;
181 $queryStatus .= $joinList;
182 }
183 }
184
185 private function handleSearchFilter(array $settings, array &$filters, string &$query, string &$queryCount, string &$queryStatus): void
186 {
187 if (empty($settings['search'])) return;
188
189 $searchValue = acym_escapeDB('%'.$settings['search'].'%');
190 $searchFilter = 'user.email LIKE '.$searchValue.' OR user.name LIKE '.$searchValue.' OR user.id = '.intval($settings['search']);
191
192 // Search in visible custom fields
193 $listingFields = acym_loadResultArray('SELECT `id` FROM #__acym_field WHERE `'.(acym_isAdmin() ? 'back' : 'front').'end_listing` = 1');
194 if (!empty($listingFields)) {
195 $query = 'DISTINCT '.$query;
196 $join = ' LEFT JOIN #__acym_user_has_field AS userfield ON user.id = userfield.user_id AND userfield.field_id IN ('.implode(', ', $listingFields).') AND ';
197 $cfSearch = 'userfield.value LIKE '.$searchValue;
198
199 $fieldsWithMultipleValues = acym_loadObjectList(
200 'SELECT *
201 FROM #__acym_field
202 WHERE id IN ('.implode(', ', $listingFields).')
203 AND `value` LIKE '.acym_escapeDB('%"title":"'.$settings['search'].'"%')
204 );
205
206 if (!empty($fieldsWithMultipleValues)) {
207 foreach ($fieldsWithMultipleValues as $oneField) {
208 $oneField->value = json_decode($oneField->value, true);
209 foreach ($oneField->value as $value) {
210 if (stripos($value['title'], $settings['search']) !== false) {
211 $cfSearch .= ' OR (userfield.field_id = '.intval($oneField->id).' AND userfield.value LIKE '.acym_escapeDB('%'.$value['value'].'%').')';
212 }
213 }
214 }
215 }
216
217 $join .= '('.$cfSearch.')';
218 $searchFilter .= ' OR userfield.field_id IS NOT NULL';
219
220 $query .= $join;
221 $queryCount .= $join;
222 $queryStatus .= $join;
223 }
224
225 $filters[] = $searchFilter;
226 }
227
228 private function handleUserStatusFilter(array $settings, array &$filters, string $queryStatus): array
229 {
230 if (!empty($filters)) {
231 $queryStatus .= ' WHERE ('.implode(') AND (', $filters).')';
232 }
233
234 $usersPerStatusObject = acym_loadObjectList($queryStatus.' GROUP BY score', 'score');
235
236 $usersPerStatus = [];
237 for ($i = 0; $i < 4; $i++) {
238 $usersPerStatus[$i] = empty($usersPerStatusObject[$i]) ? 0 : $usersPerStatusObject[$i]->number;
239 }
240
241 if (!empty($settings['status'])) {
242 $allowedStatus = [
243 'active' => 'active = 1',
244 'inactive' => 'active = 0',
245 'confirmed' => 'confirmed = 1',
246 'unconfirmed' => 'confirmed = 0',
247 ];
248 if (empty($allowedStatus[$settings['status']])) {
249 die('Injection denied');
250 }
251 $filters[] = 'user.'.$allowedStatus[$settings['status']];
252 }
253
254 return [
255 'all' => array_sum($usersPerStatus),
256 'active' => $usersPerStatus[2] + $usersPerStatus[3],
257 'inactive' => $usersPerStatus[0] + $usersPerStatus[1],
258 'confirmed' => $usersPerStatus[1] + $usersPerStatus[3],
259 'unconfirmed' => $usersPerStatus[0] + $usersPerStatus[2],
260 ];
261 }
262
263 private function handleEntitySelect(array $settings, array &$filters): ?array
264 {
265 if (isset($settings['hiddenElements'])) {
266 if (empty($settings['hiddenElements'])) {
267 $filters[] = 'user.id IS NOT NULL';
268 } else {
269 acym_arrayToInteger($settings['hiddenElements']);
270 $filters[] = 'user.id NOT IN('.implode(',', $settings['hiddenElements']).')';
271 }
272 }
273
274 if (isset($settings['onlyElements'])) {
275 if (empty($settings['onlyElements'])) {
276 $filters[] = 'user.id IS NULL';
277 } else {
278 acym_arrayToInteger($settings['onlyElements']);
279 $filters[] = 'user.id IN('.implode(',', $settings['onlyElements']).')';
280 }
281 }
282
283 // For the entity select when selecting "show only selected users"
284 if (!empty($settings['showOnlySelected'])) {
285 if (!empty($settings['selectedUsers'])) {
286 acym_arrayToInteger($settings['selectedUsers']);
287 $filters[] = 'user.id IN('.implode(',', $settings['selectedUsers']).')';
288 } else {
289 return ['users' => [], 'total' => 0];
290 }
291 }
292
293 return null;
294 }
295
296 private function handleSegmentFilter(array $settings, array &$filters, string &$query, string &$queryCount, string &$queryStatus): void
297 {
298 if (!array_key_exists('segment', $settings)) return;
299 if ($settings['segment'] == 0) return;
300
301 $segmentClass = (new SegmentClass())->getOneById($settings['segment']);
302 $automationHelpers = [];
303 if (!empty($segmentClass) && !empty($segmentClass->filters)) {
304 foreach ($segmentClass->filters as $or => $orValues) {
305 if (empty($orValues)) continue;
306 $automationHelpers[$or] = new AutomationHelper();
307 foreach ($orValues as $and => $andValues) {
308 $and = intval($and);
309 foreach ($andValues as $filterName => $options) {
310 acym_trigger('onAcymProcessFilter_'.$filterName, [&$automationHelpers[$or], &$options, $and.'_'.$or]);
311 }
312 }
313 }
314 }
315
316 $whereBlocks = [];
317 $join = '';
318 foreach ($automationHelpers as $automationHelper) {
319 if (!empty($automationHelper->where)) {
320 $whereBlocks[] = ' ('.implode(') and (', $automationHelper->where).')';
321 }
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 }
328 }
329 if (!empty($whereBlocks)) {
330 $filters[] = implode(' OR ', $whereBlocks);
331 }
332 $query .= $join;
333 $queryCount .= $join;
334 $queryStatus .= $join;
335 }
336
337 public function getJoinForQuery(string $joinType): string
338 {
339 if (strpos($joinType, 'join_list') !== false) {
340 $listId = explode('-', $joinType);
341
342 return ' LEFT JOIN #__acym_user_has_list AS userlist ON user.id = userlist.user_id AND userlist.status = 1 AND userlist.list_id = '.intval($listId[1]);
343 }
344
345 return '';
346 }
347
348 public function getOneByCMSId(?int $id): ?object
349 {
350 if (empty($id)) {
351 return null;
352 }
353
354 $user = acym_loadObject('SELECT * FROM #__acym_user WHERE cms_id = '.intval($id));
355
356 return empty($user) ? null : $user;
357 }
358
359 public function getOneByEmail(?string $email, bool $autoAdd = false): ?object
360 {
361 if (!acym_isValidEmail($email)) {
362 return null;
363 }
364
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;
382 }
383
384 public function getByColumnValue(string $column, string $value): array
385 {
386 $userColumns = acym_getColumns('user');
387 if (!in_array($column, $userColumns)) return [];
388
389 $query = 'SELECT * FROM #__acym_user WHERE '.acym_secureDBColumn($column).' = '.acym_escapeDB($value);
390
391 return acym_loadObjectList($query, $this->pkey);
392 }
393
394 public function getUserSubscriptionById(
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 {
404 $mailType = null;
405 if (!empty($mailId)) {
406 $mailType = (new MailClass())->getMailType(intval($mailId));
407 }
408
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
410 FROM #__acym_list AS list
411 JOIN #__acym_user_has_list AS userlist
412 ON list.id = userlist.list_id ';
413
414 if (!empty($mailId) && $campaignOnly && !empty($mailType) && $mailType !== MailClass::TYPE_FOLLOWUP) {
415 $query .= 'JOIN #__acym_mail_has_list AS mail_list ON list.id = mail_list.list_id AND mail_list.mail_id = '.intval($mailId);
416 }
417
418 $query .= ' WHERE userlist.user_id = '.intval($userId);
419
420 if (!$includeManagement && !empty($mailType) && $mailType === MailClass::TYPE_FOLLOWUP) {
421 $query .= ' AND list.type = '.acym_escapeDB(ListClass::LIST_TYPE_FOLLOWUP);
422 } elseif (!$includeManagement) {
423 $types = [ListClass::LIST_TYPE_STANDARD, ListClass::LIST_TYPE_FOLLOWUP];
424 $types = array_map('acym_escapeDB', $types);
425 $query .= ' AND list.type IN ('.implode(',', $types).')';
426 }
427
428 if ($visible) {
429 $query .= ' AND list.visible = 1';
430 }
431
432 if ($sortBySubscription) {
433 $query .= ' ORDER BY userlist.subscription_date DESC';
434 }
435
436 $lists = acym_loadObjectList($query, $key);
437
438 if (acym_isMultilingual() && $needTranslation) {
439 $listClass = new ListClass();
440 $lists = $listClass->getTranslatedNameDescription($lists);
441 }
442
443 return $lists;
444 }
445
446 public function getUserStandardListIdById(int $userId): array
447 {
448 $query = 'SELECT list.id FROM #__acym_list AS list
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.'
450 WHERE list.visible = 1 AND list.type = '.acym_escapeDB(ListClass::LIST_TYPE_STANDARD);
451
452 return acym_loadResultArray($query);
453 }
454
455 public function getAllListsUserSubscriptionById(int $userId, string $key = 'id', bool $needTranslation = false): array
456 {
457 $query = 'SELECT list.id, list.translation, list.name, list.color, list.active, list.visible, userlist.status, userlist.subscription_date, userlist.unsubscribe_date
458 FROM #__acym_list AS list
459 LEFT JOIN #__acym_user_has_list AS userlist
460 ON list.id = userlist.list_id
461 AND userlist.user_id = '.intval($userId);
462
463 $lists = acym_loadObjectList($query, $key);
464
465 if (acym_isMultilingual() && $needTranslation) {
466 $listClass = new ListClass();
467 $lists = $listClass->getTranslatedNameDescription($lists);
468 }
469
470 return $lists;
471 }
472
473 public function getUsersSubscriptionsByIds(array $userIds, int $creatorId = 0): array
474 {
475 acym_arrayToInteger($userIds);
476 $query = 'SELECT `list`.`id`, `list`.`color`, `list`.`name`, `userlist`.`user_id`, `userlist`.`status`
477 FROM #__acym_list AS `list`
478 JOIN #__acym_user_has_list AS `userlist`
479 ON `list`.`id` = `userlist`.`list_id`
480 WHERE `userlist`.`user_id` IN ('.implode(',', $userIds).')
481 AND `list`.`type` = '.acym_escapeDB(ListClass::LIST_TYPE_STANDARD);
482
483 if (!empty($creatorId)) {
484 $userGroups = acym_getGroupsByUser($creatorId);
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).')';
491 }
492 $query .= ' ORDER BY `userlist`.`status` DESC, `list`.`id` ASC';
493
494 return acym_loadObjectList($query);
495 }
496
497 public function getCountTotalUsers(): int
498 {
499 $numberOfUsers = acym_loadResult('SELECT COUNT(id) FROM #__acym_user');
500
501 return empty($numberOfUsers) ? 0 : $numberOfUsers;
502 }
503
504 public function getSubscriptionStatus(int $userId, array $listIds = [], ?int $wantedStatus = null): array
505 {
506 $query = 'SELECT status, list_id
507 FROM #__acym_user_has_list
508 WHERE user_id = '.intval($userId);
509 if (!empty($listIds)) {
510 acym_arrayToInteger($listIds);
511 $query .= ' AND list_id IN ('.implode(',', $listIds).')';
512 }
513
514 if ($wantedStatus !== null) {
515 $query .= ' AND status = '.intval($wantedStatus);
516 }
517
518 return acym_loadObjectList($query, 'list_id');
519 }
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 */
528 public function identify(bool $onlyValue = false, ?string $idName = null, ?string $keyName = null): ?object
529 {
530 $id = acym_getVar('int', empty($idName) ? 'id' : $idName, 0);
531 $key = acym_getVar('string', empty($keyName) ? 'key' : $keyName, '');
532
533 if (empty($id) && !empty($idName) && empty($key) && !empty($keyName)) {
534 $id = acym_getVar('int', 'id', 0);
535 $key = acym_getVar('string', 'key', 0);
536 }
537
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...
541 $currentUserid = acym_currentUserId();
542 if (!empty($currentUserid)) {
543 return $this->getOneByCMSId($currentUserid);
544 }
545 if (!$onlyValue) {
546 acym_enqueueMessage(acym_translation('ACYM_LOGIN'), 'error');
547 }
548
549 return null;
550 }
551
552 $userIdentified = acym_loadObject('SELECT * FROM #__acym_user WHERE `id` = '.intval($id).' AND `key` = '.acym_escapeDB($key));
553
554 if (!empty($userIdentified)) {
555 return $userIdentified;
556 }
557
558 if (!$onlyValue) {
559 acym_enqueueMessage(acym_translation('ACYM_USER_NOT_FOUND'), 'error');
560 }
561
562 return null;
563 }
564
565 public function subscribe($userIds, array $addLists, bool $trigger = true, bool $forceFront = false): bool
566 {
567 if (empty($addLists)) {
568 return false;
569 }
570 acym_arrayToInteger($addLists);
571
572 //TODO was previously shown as an int in the dev documentation, hence not adding the type yet
573 if (!is_array($userIds)) {
574 $userIds = [$userIds];
575 }
576 acym_arrayToInteger($userIds);
577
578 $listClass = new ListClass();
579 $historyClass = new HistoryClass();
580
581 $confirmationRequired = !empty($this->config->get('require_confirmation', 1));
582 $subscribedToLists = false;
583 $historyData = acym_translationSprintf('ACYM_LISTS_NUMBERS', implode(', ', $addLists));
584
585 foreach ($userIds as $userId) {
586 $user = $this->getOneById($userId);
587 if (empty($user)) continue;
588 $currentSubscription = $this->getUserSubscriptionById($userId, 'id', true);
589
590 $currentlySubscribed = [];
591 $currentlyUnsubscribed = [];
592 foreach ($currentSubscription as $oneList) {
593 if ($oneList->status == 1) {
594 $currentlySubscribed[$oneList->id] = $oneList;
595 } else {
596 $currentlyUnsubscribed[$oneList->id] = $oneList;
597 }
598 }
599
600 $subscribedLists = [];
601 $listsNotExisting = [];
602 foreach ($addLists as $oneListId) {
603 // The user is already subscribed
604 if (empty($oneListId) || !empty($currentlySubscribed[$oneListId])) {
605 continue;
606 }
607
608 $list = $listClass->getOneById($oneListId);
609 if (empty($list)) {
610 $listsNotExisting[] = $oneListId;
611 continue;
612 }
613
614 $subscription = new \stdClass();
615 $subscription->user_id = $userId;
616 $subscription->list_id = $oneListId;
617 $subscription->status = 1;
618 $subscription->subscription_date = gmdate('Y-m-d H:i:s', time());
619
620 if (empty($currentSubscription[$oneListId])) {
621 // The user isn't already subscribed, we subscribe him!
622 acym_insertObject('#__acym_user_has_list', $subscription);
623 } elseif (!empty($currentlyUnsubscribed[$oneListId])) {
624 // The user re-subscribes or we have to confirm the subscription
625 acym_updateObject('#__acym_user_has_list', $subscription, ['user_id', 'list_id']);
626 }
627
628 $subscribedLists[] = $oneListId;
629 $subscribedToLists = true;
630 }
631
632 if (!empty($listsNotExisting)) {
633 $this->errors['list_not_existing'] = acym_translationSprintf('ACYM_LIST_NOT_FOUND', implode(', ', $listsNotExisting));
634 }
635
636 $historyClass->insert($userId, 'subscribed', [$historyData]);
637
638 if ($trigger) {
639 acym_trigger('onAcymAfterUserSubscribe', [&$user, &$subscribedLists]);
640 }
641
642 if ($this->sendWelcomeEmail && (!$confirmationRequired || $user->confirmed == 1) && $user->active == 1) {
643 $listClass->sendWelcome($userId, $subscribedLists, $forceFront);
644 }
645 }
646
647 return $subscribedToLists;
648 }
649
650 private function registerUnsubUser(array $userIds): void
651 {
652 $mailId = acym_getVar('int', 'mail_id', 0);
653 if (empty($mailId)) return;
654
655 $mailStatClass = new MailStatClass();
656 $userStatClass = new UserStatClass();
657
658 $countUnsubscribe = 0;
659
660 foreach ($userIds as $id) {
661 $userStat = $userStatClass->getOneByMailAndUserId($mailId, $id);
662 // We didn't send this email to this user, something is wrong
663 if (empty($userStat)) continue;
664
665 $newUserStat = new \stdClass();
666 $newUserStat->mail_id = $mailId;
667 $newUserStat->user_id = $id;
668 $newUserStat->unsubscribe = empty($userStat->unsubscribe) ? 1 : $userStat->unsubscribe + 1;
669
670 if (empty($userStat->unsubscribe)) {
671 $countUnsubscribe++;
672 }
673
674 $userStatClass->save($newUserStat);
675 }
676
677 $mailStat = new \stdClass();
678 $mailStat->mail_id = $mailId;
679 $mailStat->unsubscribe_total = $countUnsubscribe;
680
681 $mailStatClass->save($mailStat);
682 }
683
684 public function unsubscribe($userIds, array $lists): bool
685 {
686 if (empty($lists)) {
687 return false;
688 }
689
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);
696
697 $listClass = new ListClass();
698 $unsubscribedFromLists = false;
699 foreach ($userIds as $userId) {
700 $user = $this->getOneById($userId);
701
702 if (empty($user)) {
703 continue;
704 }
705
706 $currentSubscription = $this->getUserSubscriptionById($userId);
707
708 $currentlySubscribed = [];
709 $currentlyUnsubscribed = [];
710 foreach ($currentSubscription as $oneList) {
711 if ($oneList->status == 0) {
712 $currentlyUnsubscribed[$oneList->id] = $oneList;
713 } elseif ($oneList->status == 1) {
714 $currentlySubscribed[$oneList->id] = $oneList;
715 }
716 }
717
718 $unsubscribedLists = [];
719 $listsNotExisting = [];
720 foreach ($lists as $oneListId) {
721 // The user is already unsubscribed
722 if (empty($oneListId) || !empty($currentlyUnsubscribed[$oneListId])) {
723 continue;
724 }
725
726 // Don't unsubscribe from non-subscribed lists
727 if (empty($currentlySubscribed[$oneListId])) {
728 continue;
729 }
730
731 $list = $listClass->getOneById($oneListId);
732 if (empty($list)) {
733 $listsNotExisting[] = $oneListId;
734 continue;
735 }
736
737 $subscription = new \stdClass();
738 $subscription->user_id = $userId;
739 $subscription->list_id = $oneListId;
740 $subscription->status = 0;
741 $subscription->unsubscribe_date = gmdate('Y-m-d H:i:s', time());
742 if (empty($currentSubscription[$oneListId])) {
743 // The user isn't already subscribed, we unsubscribe him directly
744 acym_insertObject('#__acym_user_has_list', $subscription);
745 } else {
746 // The user unsubscribed from a list
747 acym_updateObject('#__acym_user_has_list', $subscription, ['user_id', 'list_id']);
748 }
749
750 $unsubscribedLists[] = $oneListId;
751 $unsubscribedFromLists = true;
752 }
753
754 if (!empty($listsNotExisting)) {
755 $this->errors['list_not_existing'] = acym_translationSprintf('ACYM_LIST_NOT_FOUND', implode(', ', $listsNotExisting));
756 }
757
758 acym_query(
759 'DELETE FROM #__acym_queue WHERE user_id = '.intval($userId).' AND mail_id IN (
760 SELECT followup_mail.mail_id FROM #__acym_followup_has_mail AS followup_mail
761 JOIN #__acym_followup AS followup ON followup.id = followup_mail.followup_id AND followup.list_id IN ('.implode(',', $lists).')
762 )'
763 );
764
765 $historyClass = new HistoryClass();
766 $historyData = acym_translationSprintf('ACYM_LISTS_NUMBERS', implode(', ', $lists));
767 $historyClass->insert($userId, 'unsubscribed', [$historyData], 0, acym_getVar('string', 'unsubscribe_reason'));
768
769 if ($this->triggers) {
770 acym_trigger('onAcymAfterUserUnsubscribe', [&$user, &$unsubscribedLists]);
771 }
772
773 if ($this->sendUnsubscribeEmail) {
774 $listClass->sendUnsubscribe($userId, $unsubscribedLists);
775 }
776
777 if (!empty($unsubscribedLists)) {
778 foreach ($unsubscribedLists as $i => $oneListId) {
779 $currentList = $listClass->getOneById($oneListId);
780 $unsubscribedLists[$i] = $currentList->name;
781 }
782 $this->sendNotification($user->id, 'acy_notification_unsub', ['lists' => implode(', ', $unsubscribedLists)]);
783 }
784 }
785
786 if ($unsubscribedFromLists) {
787 $this->registerUnsubUser($userIds);
788 }
789
790 return $unsubscribedFromLists;
791 }
792
793 public function unsubscribeOnSubscriptions(int $userId, array $listIds): bool
794 {
795 if (empty($listIds)) {
796 return false;
797 }
798
799 acym_arrayToInteger($listIds);
800
801 $subscribedLists = acym_loadResultArray('SELECT list_id FROM #__acym_user_has_list WHERE user_id = '.intval($userId).' AND list_id IN ('.implode(',', $listIds).')');
802
803 if (!empty($subscribedLists)) $this->unsubscribe([$userId], $subscribedLists);
804
805 return true;
806 }
807
808 public function removeSubscription(array $userIds, array $listIds = []): void
809 {
810 if (empty($listIds) || empty($userIds)) {
811 return;
812 }
813
814 acym_arrayToInteger($userIds);
815 acym_arrayToInteger($listIds);
816
817 acym_query(
818 'DELETE FROM #__acym_user_has_list
819 WHERE user_id IN ('.implode(',', $userIds).')
820 AND list_id IN ('.implode(',', $listIds).')'
821 );
822 }
823
824 public function onlyManageableUsers(array &$elements): void
825 {
826 if (acym_isAdmin() && acym_isAllowed('users')) {
827 return;
828 }
829
830 $listClass = new ListClass();
831 $manageableLists = $listClass->getManageableLists();
832 if (empty($manageableLists)) {
833 return;
834 }
835
836 $currentUserId = acym_currentUserId();
837 acym_arrayToInteger($elements);
838
839 $selfManagement = in_array($currentUserId, $elements);
840
841 $elements = acym_loadResultArray(
842 'SELECT DISTINCT user_id
843 FROM #__acym_user_has_list
844 WHERE `list_id` IN ('.implode(',', $manageableLists).')
845 AND `user_id` IN ('.implode(',', $elements).')'
846 );
847
848 if ($selfManagement && !in_array($currentUserId, $elements)) {
849 $elements[] = $currentUserId;
850 }
851 }
852
853 public function delete(array $elements, bool $forceDelete = false): int
854 {
855 acym_arrayToInteger($elements);
856 $this->onlyManageableUsers($elements);
857
858 if (empty($elements)) {
859 return 0;
860 }
861
862 if (acym_isAdmin() || $forceDelete || 'delete' === $this->config->get('frontend_delete_button', 'delete')) {
863 acym_query('DELETE FROM #__acym_user_has_list WHERE user_id IN ('.implode(',', $elements).')');
864 acym_query('DELETE FROM #__acym_queue WHERE user_id IN ('.implode(',', $elements).')');
865 acym_query('DELETE FROM #__acym_user_has_field WHERE user_id IN ('.implode(',', $elements).')');
866 acym_query('DELETE FROM #__acym_history WHERE user_id IN ('.implode(',', $elements).')');
867 acym_query('DELETE FROM #__acym_user_stat WHERE user_id IN ('.implode(',', $elements).')');
868
869 $scenarioProcessIds = acym_loadResultArray('SELECT id FROM #__acym_scenario_process WHERE user_id IN ('.implode(',', $elements).')');
870
871 if (!empty($scenarioProcessIds)) {
872 acym_query('DELETE FROM #__acym_scenario_history_line WHERE scenario_process_id IN ('.implode(',', $scenarioProcessIds).')');
873 acym_query('DELETE FROM #__acym_scenario_queue WHERE scenario_process_id IN ('.implode(',', $scenarioProcessIds).')');
874 acym_query('DELETE FROM #__acym_scenario_process WHERE id IN ('.implode(',', $scenarioProcessIds).')');
875 }
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
885 return parent::delete($elements);
886 } else {
887 $listClass = new ListClass();
888 $manageableLists = $listClass->getManageableLists();
889
890 if (!empty($manageableLists)) {
891 $this->removeSubscription($elements, $manageableLists);
892 acym_query(
893 'DELETE queue
894 FROM #__acym_queue AS queue
895 JOIN #__acym_mail_has_list AS maillist
896 ON queue.mail_id = maillist.mail_id
897 WHERE queue.user_id IN ('.implode(',', $elements).')
898 AND maillist.list_id IN ('.implode(',', $manageableLists).')'
899 );
900 }
901
902 return count($elements);
903 }
904 }
905
906 public function save(object $user, array $customFields = [], bool $ajax = false): ?int
907 {
908 if (empty($user->email) && empty($user->id)) {
909 return null;
910 }
911
912 if (isset($user->email)) {
913 $user->email = strtolower($user->email);
914 if (!acym_isValidEmail($user->email)) {
915 $this->errors[] = acym_translation('ACYM_VALID_EMAIL');
916
917 return null;
918 }
919 }
920
921 if (empty($user->id)) {
922 if (!isset($user->active)) {
923 $user->active = 1;
924 }
925
926 if (!isset($user->tracking)) {
927 $user->tracking = $this->config->get('dont_track_by_default', 0) ? 0 : 1;
928 }
929
930 if (empty($user->language)) {
931 // Take the user account's language
932 if (!acym_isAdmin()) {
933 $cmsUserLanguage = acym_getCmsUserLanguage();
934 }
935
936 if (!empty($cmsUserLanguage)) {
937 $user->language = $cmsUserLanguage;
938 } else {
939 $user->language = acym_getDefaultUserLanguage();
940 }
941 }
942
943 $currentUserid = acym_currentUserId();
944 $currentEmail = acym_currentUserEmail();
945 $allowVisitors = $this->config->get('allow_visitor', 1) == 1;
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
948 $this->errors[] = acym_translation('ACYM_ONLY_LOGGED');
949
950 return null;
951 }
952
953 if (empty($user->name) && $this->config->get('generate_name', 1)) {
954 $user->name = ucwords(trim(str_replace(['.', '_', ')', ',', '(', '-', 1, 2, 3, 4, 5, 6, 7, 8, 9, 0], ' ', substr($user->email, 0, strpos($user->email, '@')))));
955 }
956
957 $source = acym_getVar('string', 'acy_source', '');
958 if (empty($user->source) && !empty($source)) {
959 $user->source = $source;
960 }
961
962 $user->key = acym_generateKey(14);
963
964 $user->creation_date = gmdate('Y-m-d H:i:s', time());
965 } elseif (!empty($user->confirmed)) {
966 $oldUser = $this->getOneByIdWithCustomFields($user->id);
967 if (!empty($oldUser) && empty($oldUser['confirmed'])) {
968 $user->confirmation_date = gmdate('Y-m-d H:i:s', time());
969 $user->confirmation_ip = acym_getIP();
970
971 if ($this->triggers) {
972 acym_trigger('onAcymAfterUserConfirm', [&$user]);
973 }
974 }
975 } else {
976 $oldUser = $this->getOneByIdWithCustomFields($user->id);
977 }
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
989 foreach ($user as $oneAttribute => $value) {
990 if (empty($value)) {
991 continue;
992 }
993
994 $oneAttribute = trim(strtolower($oneAttribute));
995 if (!in_array($oneAttribute, self::RESTRICTED_FIELDS)) {
996 $user->$oneAttribute = acym_stripTags($value);
997 }
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
1001 if (is_numeric($user->$oneAttribute)) {
1002 continue;
1003 }
1004
1005 if (function_exists('mb_detect_encoding')) {
1006 if (mb_detect_encoding($user->$oneAttribute, 'UTF-8', true) !== 'UTF-8') {
1007 $user->$oneAttribute = acym_utf8Encode($user->$oneAttribute);
1008 }
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 ) {
1015 $user->$oneAttribute = acym_utf8Encode($user->$oneAttribute);
1016 }
1017 }
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
1024 if (empty($user->id)) {
1025 if (empty($user->cms_id) && !empty($user->email)) {
1026 global $acymCmsUserVars;
1027 $userCmsID = acym_loadResult(
1028 'SELECT '.acym_secureDBColumn($acymCmsUserVars->id).' FROM '.$acymCmsUserVars->table.' WHERE '.acym_secureDBColumn(
1029 $acymCmsUserVars->email
1030 ).' = '.acym_escapeDB($user->email)
1031 );
1032
1033 if (!empty($userCmsID)) $user->cms_id = $userCmsID;
1034 }
1035 if ($this->triggers) {
1036 $result = acym_trigger('onAcymBeforeUserCreate', [&$user]);
1037 if (is_array($result) && in_array(false, $result)) {
1038 $acycheckerError = acym_getVar('string', 'acychecker_error');
1039 if (!empty($acycheckerError)) {
1040 if ($ajax) {
1041 $this->errors[] = $acycheckerError;
1042 } else {
1043 acym_enqueueMessage($acycheckerError, 'error');
1044 }
1045 }
1046
1047 return null;
1048 }
1049 }
1050 } else {
1051 if ($this->triggers) {
1052 acym_trigger('onAcymBeforeUserModify', [&$user]);
1053 }
1054 }
1055
1056 $userID = parent::save($user);
1057
1058 // Save custom fields if there are any
1059 $fieldClass = new FieldClass();
1060 if (!empty($customFields)) {
1061 foreach ($customFields as $key => $value) {
1062 if (!is_string($value)) {
1063 continue;
1064 }
1065
1066 // Remove most of the existing emojis that are not supported
1067 $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $value);
1068
1069 $field = $fieldClass->getOneById($key);
1070 if (empty($field) || $field->type !== 'textarea') {
1071 $value = preg_replace('/\s+/', ' ', $value);
1072 }
1073 $customFields[$key] = $value;
1074 }
1075 }
1076 $fieldClass->store($userID, $customFields, $ajax);
1077 if (!empty($fieldClass->errors)) {
1078 $this->errors = array_merge($this->errors, $fieldClass->errors);
1079 }
1080
1081 $historyClass = new HistoryClass();
1082 if (empty($user->id)) {
1083 $user->id = $userID;
1084 $historyClass->insert($user->id, 'created');
1085 if ($this->triggers) {
1086 acym_trigger('onAcymAfterUserCreate', [&$user]);
1087 acym_triggerCmsHook('onAcymAfterUserSave', [$user, true]);
1088 }
1089 } else {
1090 $historyClass->insert($user->id, 'modified');
1091 if ($this->triggers) {
1092 acym_trigger('onAcymAfterUserModify', [&$user, &$oldUser]);
1093 acym_triggerCmsHook('onAcymAfterUserSave', [$user, false]);
1094 }
1095 }
1096
1097 $this->sendConfirmation($userID);
1098
1099 return $userID;
1100 }
1101
1102 public function saveForm(bool $ajax = false): bool
1103 {
1104 $allowUserModifications = $this->config->get('allow_modif', 'data') === 'all' || $this->allowModif;
1105 $allowSubscriptionModifications = $this->config->get('allow_modif', 'data') !== 'none' || $this->allowModif;
1106
1107 $user = new \stdClass();
1108 $connectedUser = $this->identify(true, 'userId', 'userKey');
1109
1110 $userData = acym_getVar('array', 'user', []);
1111 if (!empty($userData)) {
1112 foreach ($userData as $attribute => $value) {
1113 if (!in_array($attribute, self::FORM_ALLOWED_FIELDS, true)) {
1114 continue;
1115 }
1116 $user->$attribute = $value;
1117 }
1118 }
1119
1120 if (!empty($connectedUser->id)) {
1121 $user->id = $connectedUser->id;
1122 }
1123
1124 if (empty($user->email)) {
1125 if (!empty($connectedUser->email)) {
1126 $user->email = $connectedUser->email;
1127 }
1128 }
1129
1130 if (empty($user->email)) {
1131 $this->errors[] = acym_translation('ACYM_VALID_EMAIL');
1132
1133 return false;
1134 }
1135
1136 if (!$allowUserModifications && !empty($connectedUser)) {
1137 if ($connectedUser->email != $user->email) {
1138 $this->errors[] = acym_translation('ACYM_NOT_ALLOWED_MODIFY_USER');
1139
1140 return false;
1141 }
1142
1143 $allowUserModifications = true;
1144 $allowSubscriptionModifications = true;
1145 }
1146
1147 // Check if it already exists...
1148 if (!empty($user->email)) {
1149 // Do we already have a user with this same e-mail address?
1150 if (empty($user->id)) {
1151 $user->id = 0;
1152 }
1153 $existUser = acym_loadObject('SELECT * FROM #__acym_user WHERE email = '.acym_escapeDB($user->email).' AND id != '.intval($user->id));
1154 if (!empty($existUser->id)) {
1155 if (!$this->allowModif && !$allowSubscriptionModifications) {
1156 $this->errors[] = acym_translation('ACYM_ADDRESS_TAKEN');
1157
1158 return false;
1159 } else {
1160 $user->id = $existUser->id;
1161 }
1162 }
1163 }
1164
1165 // Did the user ask for its e-mail address to be modified?
1166 // If so, we set it as not confirmed automatically.
1167 if (!empty($user->id) && !empty($user->email)) {
1168 $existUser = $this->getOneById($user->id);
1169 if (trim(strtolower($user->email)) != strtolower($existUser->email)) {
1170 $user->confirmed = 0;
1171 }
1172 }
1173
1174 $fromProfile = acym_getVar('int', 'acyprofile', 0) == 1;
1175
1176 $this->newUser = empty($user->id);
1177 if (empty($user->id) || $allowUserModifications) {
1178 if ($this->newUser && $this->config->get('require_confirmation', 1) == 1) {
1179 $user->confirmed = 0;
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
1187 $customFieldData = acym_getVar('array', 'customField', []);
1188 $id = $this->save($user, $customFieldData, $ajax);
1189 $allowSubscriptionModifications = true;
1190 } else {
1191 $id = $user->id;
1192 //We didn't save the user... but we should check the confirm field anyway
1193 if (isset($user->confirmed) && empty($user->confirmed)) {
1194 $this->sendConfirmation($id);
1195 }
1196 }
1197
1198 if (empty($id)) return false;
1199
1200 $formData = acym_getVar('array', 'data', []);
1201 acym_setVar('userId', $id);
1202
1203 if (!acym_isAdmin()) {
1204 $hiddenlistsString = acym_getVar('string', 'hiddenlists', '');
1205 $hiddenlists = explode(',', $hiddenlistsString);
1206 $visibleSubscription = acym_getVar('array', 'subscription', []);
1207 $subscribeLists = array_merge($hiddenlists, $visibleSubscription);
1208 acym_arrayToInteger($subscribeLists);
1209
1210 if (!empty($subscribeLists)) {
1211 foreach ($subscribeLists as $oneListId) {
1212 $formData['listsub'][$oneListId] = ['status' => 1];
1213 }
1214 }
1215 }
1216
1217 if (empty($formData['listsub'])) {
1218 if (!$fromProfile) $this->sendNotification($id, 'acy_notification_subform');
1219
1220 $this->sendNotification(
1221 $id,
1222 $this->newUser ? 'acy_notification_create' : 'acy_notification_profile'
1223 );
1224
1225 return true;
1226 }
1227
1228 if (!$allowSubscriptionModifications) {
1229 $this->requireId = true;
1230 $this->errors[] = acym_translation('ACYM_NOT_ALLOWED_MODIFY_USER');
1231
1232 return false;
1233 }
1234
1235 $addLists = [];
1236 $unsubLists = [];
1237 foreach ($formData['listsub'] as $listID => $oneList) {
1238 if ($oneList['status'] == 1) {
1239 $addLists[] = $listID;
1240 } else {
1241 $unsubLists[] = $listID;
1242 }
1243 }
1244
1245 $this->subscribed = $this->subscribe([$id], $addLists);
1246
1247 if (!$fromProfile) $this->sendNotification($id, 'acy_notification_subform');
1248
1249 $this->sendNotification(
1250 $id,
1251 $this->newUser ? 'acy_notification_create' : 'acy_notification_profile'
1252 );
1253
1254 if (!$this->newUser) $this->unsubscribe([$id], $unsubLists);
1255
1256 return true;
1257 }
1258
1259 public function sendConfirmation(int $userID): void
1260 {
1261 if (!$this->forceConf && !$this->sendConf) {
1262 return;
1263 }
1264
1265 if ($this->config->get('require_confirmation', 1) != 1 || (acym_isAdmin() && !$this->forceConfAdmin)) {
1266 return;
1267 }
1268
1269 $myUser = $this->getOneById($userID);
1270
1271 if (!empty($myUser->confirmed)) {
1272 return;
1273 }
1274
1275 $mailerHelper = new MailerHelper();
1276 $mailerHelper->report = (bool)$this->config->get('confirm_message', 0);
1277
1278 //TODO $mailerHelper->addParam('user:subscription', implode('<br/>', $subscription));
1279 $mailClass = new MailClass();
1280 $confirmationEmail = $mailClass->getOneByName('acy_confirm');
1281 $this->confirmationSentSuccess = !empty($confirmationEmail) && $mailerHelper->sendOne($confirmationEmail->id, $myUser->id);
1282 $this->confirmationSentError = $mailerHelper->reportMessage;
1283 }
1284
1285 public function setInactive(array $elements): void
1286 {
1287 $this->deactivate($elements);
1288 }
1289
1290 public function deactivate(array $userIds): void
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 {
1317 $user = $this->getOneById($userId);
1318 if (empty($user)) return;
1319
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());
1322 $ip = acym_getIP();
1323 $query = 'UPDATE `#__acym_user`';
1324 $query .= ' SET `confirmed` = 1, `confirmation_date` = '.acym_escapeDB($confirmDate).', `confirmation_ip` = '.acym_escapeDB($ip);
1325 $query .= ' WHERE `id` = '.intval($userId).' LIMIT 1';
1326
1327 try {
1328 $res = acym_query($query);
1329 } catch (\Exception $e) {
1330 $res = false;
1331 }
1332 if ($res === false) {
1333 $errorMessage = isset($e) ? $e->getMessage() : acym_getDBError();
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).'...';
1336 acym_display($msg, 'error');
1337 exit;
1338 }
1339
1340 acym_trigger('onAcymAfterUserConfirm', [&$user]);
1341 $this->sendNotification($userId, 'acy_notification_confirm');
1342
1343 $historyClass = new HistoryClass();
1344 $historyClass->insert($userId, 'confirmed');
1345
1346 if (empty($this->config->get('require_confirmation', 1))) return;
1347
1348 $listIDs = acym_loadResultArray('SELECT `list_id` FROM `#__acym_user_has_list` WHERE `status` = 1 AND `user_id` = '.intval($userId));
1349
1350 if (empty($listIDs)) return;
1351
1352 $listClass = new ListClass();
1353 $listClass->sendWelcome($userId, $listIDs);
1354 }
1355
1356 public function getOneByIdWithCustomFields(int $id): array
1357 {
1358 $user = $this->getOneById($id);
1359 if (empty($user)) {
1360 return [];
1361 }
1362
1363 $user = get_object_vars($user);
1364
1365 $fieldsValue = acym_loadObjectList(
1366 'SELECT user_field.`value`, field.`name`
1367 FROM #__acym_user_has_field AS `user_field`
1368 LEFT JOIN #__acym_field AS `field` ON user_field.`field_id` = field.`id`
1369 WHERE user_field.`user_id` = '.intval($id),
1370 'name'
1371 );
1372
1373 foreach ($fieldsValue as $key => $value) {
1374 $fieldsValue[$key] = $value->value;
1375 }
1376
1377 return array_merge($user, $fieldsValue);
1378 }
1379
1380 public function getCustomFieldValueById(int $id): array
1381 {
1382 $fieldsValue = acym_loadObjectList(
1383 'SELECT user_field.value AS value, field.name AS name, field.type AS type, field.value AS field_params, field.id AS id
1384 FROM #__acym_user_has_field as user_field
1385 LEFT JOIN #__acym_field as field ON user_field.field_id = field.id
1386 WHERE user_field.user_id = '.intval($id),
1387 'id'
1388 );
1389
1390 $fieldReturn = [];
1391
1392 foreach ($fieldsValue as $fieldId => $field) {
1393 if (in_array($field->type, ['checkbox', 'radio', 'single_dropdown', 'multiple_dropdown'])) {
1394 $field->field_params = json_decode($field->field_params, true);
1395 $field->value = explode(',', $field->value);
1396 $values = [];
1397 foreach ($field->value as $oneValue) {
1398 $key = array_search($oneValue, array_column($field->field_params, 'value'));
1399 $values[] = $field->field_params[$key]['title'];
1400 }
1401 $fieldReturn[$field->name] = implode(',', $values);
1402 } else {
1403 $fieldReturn[$field->name] = $field->value;
1404 }
1405 }
1406
1407 return $fieldReturn;
1408 }
1409
1410 public function getAllColumnsUserAndCustomField(bool $inAction = false, bool $withSubInfos = false): array
1411 {
1412 $return = [];
1413
1414 // Basic fields in the user table
1415 $userFields = acym_getColumns('user');
1416 foreach ($userFields as $value) {
1417 $return[$value] = $value;
1418 }
1419
1420 $languageFieldId = $this->config->get(FieldClass::LANGUAGE_FIELD_ID_KEY, 0);
1421
1422 // Acy custom fields except name, email and language because they already are in the user table
1423 $customFields = acym_loadObjectList(
1424 'SELECT * FROM #__acym_field WHERE id NOT IN (1, 2, '.intval($languageFieldId).') '.($inAction ? 'AND type != "phone"' : ''),
1425 'id'
1426 );
1427 if (!empty($customFields)) {
1428 foreach ($customFields as $key => $value) {
1429 $return[$key] = $value->name;
1430 }
1431 }
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
1447 return $return;
1448 }
1449
1450 public function getAllUserFields(object $user): object
1451 {
1452 if (empty($user->id)) {
1453 return $user;
1454 }
1455
1456 $query = 'SELECT field.*, field.value AS field_value, userfield.*
1457 FROM #__acym_field AS field
1458 LEFT JOIN #__acym_user_has_field AS userfield ON field.id = userfield.field_id AND userfield.user_id = '.intval($user->id).'
1459 WHERE field.id NOT IN(1, 2)';
1460
1461 $allFields = acym_loadObjectList($query);
1462
1463 foreach ($allFields as $oneField) {
1464 $fieldOptions = @json_decode(empty($oneField->option) ? '{}' : $oneField->option);
1465 if (in_array($oneField->type, ['multiple_dropdown', 'radio', 'checkbox', 'single_dropdown'])) {
1466 $oneField->field_value = json_decode($oneField->field_value, true);
1467 if (!empty($oneField->value)) {
1468 $oneField->value = explode(',', $oneField->value);
1469 $values = [];
1470 foreach ($oneField->field_value as $oneFieldValue) {
1471 foreach ($oneField->value as $oneValue) {
1472 if ($oneFieldValue['value'] == $oneValue) {
1473 $values[] = $oneFieldValue['title'];
1474 }
1475 }
1476 }
1477 $oneField->value = implode(',', $values);
1478 }
1479 } elseif ($oneField->type === 'file' && !empty($oneField->value)) {
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;
1483 }
1484 } elseif ($oneField->type === 'date') {
1485 if (empty($fieldOptions->format)) {
1486 acym_logError('Date field with ID '.$oneField->id.' without format option');
1487 continue;
1488 }
1489 $formatToDisplay = explode('%', str_replace('y', 'Y', $fieldOptions->format));
1490 unset($formatToDisplay[0]);
1491 if (!empty($oneField->value)) {
1492 $oneField->value = acym_date($oneField->value, implode('-', $formatToDisplay));
1493 }
1494 }
1495 $user->{$oneField->namekey} = empty($oneField->value) ? '' : $oneField->value;
1496 }
1497
1498 return $user;
1499 }
1500
1501 public function getAllSimpleData(): array
1502 {
1503 return acym_loadObjectList('SELECT email, name FROM #__acym_user');
1504 }
1505
1506 public function synchSaveCmsUser(array $user, bool $isnew, ?array $oldUser = null): void
1507 {
1508 // If the source is not already defined, we define it here
1509 $source = acym_getVar('string', 'acy_source', '');
1510 if (empty($source)) acym_setVar('acy_source', ACYM_CMS);
1511
1512 if (!$this->config->get('regacy', 0)) return;
1513
1514 $this->checkVisitor = false;
1515 $this->sendConf = false;
1516
1517 $regacyForceConf = $this->config->get('regacy_forceconf', 0);
1518 /* * * * * * * * * * * * * * * * * * *
1519 * Step 1: create / update the user *
1520 * * * * * * * * * * * * * * * * * * */
1521 $cmsUser = new \stdClass();
1522 $cmsUser->email = trim(acym_stripTags($user['email']));
1523 if (!acym_isValidEmail($cmsUser->email)) return;
1524 if (!empty($user['name'])) $cmsUser->name = trim(acym_stripTags($user['name']));
1525 if (!$regacyForceConf) $cmsUser->confirmed = 1;
1526 $cmsUser->active = 1 - intval($user['block']);
1527 $cmsUser->cms_id = $user['id'];
1528
1529 if (!$isnew && !empty($oldUser['email']) && $user['email'] != $oldUser['email']) {
1530 // The user changed its email address, load the current Acy user if any
1531 $acyUser = $this->getOneByEmail($oldUser['email']);
1532 if (!empty($acyUser)) $cmsUser->id = $acyUser->id;
1533 }
1534
1535 // Just in case of this is an existing user but the e-mail address has been modified by something else
1536 if (empty($cmsUser->id) && !empty($cmsUser->cms_id)) {
1537 $acyUser = $this->getOneByCMSId($cmsUser->cms_id);
1538 if (!empty($acyUser)) $cmsUser->id = $acyUser->id;
1539 }
1540
1541 $acyUser = $this->getOneByEmail($cmsUser->email);
1542 // If an Acy user with the same email address already exists
1543 if (!empty($acyUser)) {
1544 // And wasn't linked to the site account, link it
1545 if (empty($cmsUser->id)) {
1546 $cmsUser->id = $acyUser->id;
1547 } elseif ($cmsUser->id != $acyUser->id) {
1548 // And has a different id, delete it
1549 $this->delete([$acyUser->id]);
1550 }
1551 } else {
1552 $cmsUser->source = $source;
1553 }
1554
1555 $isnew = $isnew || empty($cmsUser->id);
1556
1557 $id = $this->save($cmsUser);
1558 if (empty($id)) {
1559 return;
1560 }
1561
1562 // Force trigger confirmation process on cms user confirmation (send welcome emails, automation, save history...)
1563 $confirmationRequired = $this->config->get('require_confirmation', 1);
1564 if (!$isnew && !$regacyForceConf && $user['block'] == 0 && !empty($oldUser['block']) && $confirmationRequired == 1) {
1565 $this->confirm($id);
1566 }
1567
1568 /* * * * * * * * * * * * * * * * * * * * * *
1569 * Step 2: Handle the user's subscription *
1570 * * * * * * * * * * * * * * * * * * * * * */
1571
1572 $currentSubscription = $this->getSubscriptionStatus($id);
1573
1574 // In the Acy configuration, we can tell AcyMailing to automatically subscribe newly created users to some lists
1575 $autoLists = $isnew ? $this->config->get('regacy_autolists', '') : '';
1576 $autoLists = explode(',', $autoLists);
1577 acym_arrayToInteger($autoLists);
1578
1579 $listsClass = new ListClass();
1580 $allLists = $listsClass->getAll();
1581
1582 // The user can select some lists on the registration form
1583 $visibleLists = acym_getVar('string', 'regacy_visible_lists', '');
1584 $visibleLists = explode(',', $visibleLists);
1585 acym_arrayToInteger($visibleLists);
1586
1587 $visibleListsChecked = acym_getVar('array', 'regacy_visible_lists_checked', []);
1588 acym_arrayToInteger($visibleListsChecked);
1589
1590
1591 // Handle the unsubscription
1592 if (!$isnew && !empty($visibleLists)) {
1593 $currentlySubscribedLists = [];
1594 foreach ($currentSubscription as $oneSubscription) {
1595 if ($oneSubscription->status == 1) $currentlySubscribedLists[] = $oneSubscription->list_id;
1596 }
1597 $unsubscribeLists = array_intersect($currentlySubscribedLists, array_diff($visibleLists, $visibleListsChecked));
1598 $this->unsubscribe([$id], $unsubscribeLists);
1599 }
1600
1601 // Handle the subscription
1602 $listsToSubscribe = [];
1603 foreach ($allLists as $oneList) {
1604 if (!$oneList->active) continue;
1605 if (!empty($currentSubscription[$oneList->id]) && $currentSubscription[$oneList->id]->status == 1) continue;
1606
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 ) {
1614 $listsToSubscribe[] = $oneList->id;
1615 }
1616 }
1617
1618 if (!empty($listsToSubscribe)) $this->subscribe([$id], $listsToSubscribe);
1619
1620 if ($isnew) $this->sendNotification($id, 'acy_notification_create');
1621
1622 // We don't force the confirmation email, or the user is disabled, or he's already confirmed
1623 $acymailingUser = $this->getOneById($id);
1624 if (!empty($user['block']) || !empty($acymailingUser->confirmed)) return;
1625
1626 // New active user, or just activated the user, send the email
1627 if ($isnew || !empty($oldUser['block'])) {
1628 if ($confirmationRequired && $regacyForceConf) {
1629 $this->forceConf = true;
1630 $this->sendConfirmation($id);
1631 }
1632
1633 // Send welcome emails on CMS user confirmation (no Acym confirmation required)
1634 if (!$confirmationRequired && !empty($oldUser['email'])) {
1635 $listIDs = acym_loadResultArray('SELECT `list_id` FROM `#__acym_user_has_list` WHERE `status` = 1 AND `user_id` = '.intval($id));
1636 if (empty($listIDs)) return;
1637 $listsClass->sendWelcome($id, $listIDs);
1638 }
1639 }
1640 }
1641
1642 public function synchDeleteCmsUser(string $userEmail): void
1643 {
1644 $acyUser = $this->getOneByEmail($userEmail);
1645
1646 if (empty($acyUser)) return;
1647
1648 if ($this->config->get('regacy', '0') == 1 && $this->config->get('regacy_delete', '0') == 1) {
1649 $this->delete([$acyUser->id]);
1650 } else {
1651 acym_query('UPDATE #__acym_user SET `cms_id` = 0 WHERE `id` = '.intval($acyUser->id));
1652 }
1653 }
1654
1655 /**
1656 * Search users via partial email address
1657 */
1658 public function getUsersLikeEmail(string $search): array
1659 {
1660 $query = 'SELECT id, email FROM #__acym_user WHERE email LIKE '.acym_escapeDB('%'.$search.'%');
1661
1662 return acym_loadObjectList($query);
1663 }
1664
1665 public function sendNotification(int $userId, string $notification, array $params = []): void
1666 {
1667 if (empty($userId) || $this->blockNotifications) return;
1668
1669 $notifyUsers = explode(',', $this->config->get($notification));
1670 if (acym_isAdmin() || empty($notifyUsers)) return;
1671
1672 $mailerHelper = new MailerHelper();
1673 $mailerHelper->report = false;
1674 $mailerHelper->autoAddUser = true;
1675
1676 $user = $this->getOneById($userId);
1677 $userField = empty($user) ? null : $this->getAllUserFields($user);
1678 if (!empty($userField)) {
1679 foreach ($userField as $map => $value) {
1680 $mailerHelper->addParam('user:'.$map, $value);
1681 }
1682 }
1683
1684 // Load the subscription
1685 $rawSubscription = $this->getUserSubscriptionById($userId);
1686 $subscription = [''];
1687 foreach ($rawSubscription as $listId => $listData) {
1688 $currentList = $listData->name.' => ';
1689 if (intval($listData->status) === 1) {
1690 $currentList .= acym_translation('ACYM_SUBSCRIBED').' - '.$listData->subscription_date;
1691 } else {
1692 $currentList .= acym_translation('ACYM_UNSUBSCRIBED').' - '.$listData->unsubscribe_date;
1693 }
1694 $subscription[] = $currentList;
1695 }
1696 $mailerHelper->addParam('user:subscription', implode('<br/>', $subscription));
1697
1698 if (!empty($params)) {
1699 foreach ($params as $name => $value) {
1700 $mailerHelper->addParam($name, $value);
1701 }
1702 }
1703
1704 $mailClass = new MailClass();
1705 foreach ($notifyUsers as $oneUser) {
1706 if (!acym_isValidEmail($oneUser)) {
1707 continue;
1708 }
1709
1710 $admin = $this->getOneByEmail($oneUser, true);
1711
1712 $notificationEmail = $mailClass->getOneByName($notification);
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 }
1719 }
1720 }
1721 }
1722
1723 public function getMailHistory(int $userId)
1724 {
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 );
1736 $mailClass = new MailClass();
1737
1738 return $mailClass->decode($mailHistory);
1739 }
1740
1741 public function deleteHistoryPeriod(): array
1742 {
1743 if (empty($this->config->get('delete_user_history_enabled', 0))) return [];
1744 $deleteOverSecond = $this->config->get('delete_user_history', 0);
1745 if (empty($deleteOverSecond)) return [];
1746 $date = time() - $deleteOverSecond;
1747
1748 $query = 'DELETE FROM #__acym_history WHERE date < '.intval($date);
1749
1750 try {
1751 $status = acym_query($query);
1752 $message = empty($status) ? '' : acym_translationSprintf('ACYM_DELETE_X_ROWS_TABLE_X', $status, strtolower(acym_translation('ACYM_USER_HISTORY')));
1753 } catch (\Exception $e) {
1754 $status = false;
1755 $message = $e->getMessage();
1756 }
1757
1758 return ['status' => $status !== false, 'message' => $message];
1759 }
1760
1761 public function resetSubscription(array $userIds, array $lists): bool
1762 {
1763 if (empty($lists)) return false;
1764
1765 acym_arrayToInteger($userIds);
1766 acym_arrayToInteger($lists);
1767
1768 foreach ($userIds as $userId) {
1769 acym_query(
1770 'DELETE FROM `#__acym_user_has_list`
1771 WHERE user_id = '.$userId.'
1772 AND list_id IN ('.implode(',', $lists).')'
1773 );
1774
1775 acym_query(
1776 'DELETE FROM #__acym_queue WHERE user_id = '.$userId.' AND mail_id IN (
1777 SELECT followup_mail.mail_id FROM #__acym_followup_has_mail AS followup_mail
1778 JOIN #__acym_followup AS followup ON followup.id = followup_mail.followup_id AND followup.list_id IN ('.implode(',', $lists).')
1779 )'
1780 );
1781
1782 $historyClass = new HistoryClass();
1783 $historyData = acym_translationSprintf('ACYM_LISTS_NUMBERS', implode(', ', $lists));
1784 $historyClass->insert($userId, 'reset_subscription', [$historyData]);
1785 }
1786
1787 return true;
1788 }
1789
1790 public function addMissingKeys(): int
1791 {
1792 $usersMissingKey = acym_loadResultArray('SELECT `id` FROM #__acym_user WHERE `key` IS NULL OR `key` = ""');
1793 foreach ($usersMissingKey as $oneUserId) {
1794 acym_query('UPDATE #__acym_user SET `key` = '.acym_escapeDB(acym_generateKey(14)).' WHERE `id` = '.intval($oneUserId));
1795 }
1796
1797 return count($usersMissingKey);
1798 }
1799
1800 public function hasUserAccess(int $subscriberId): bool
1801 {
1802 $userId = acym_currentUserId();
1803 if (empty($userId)) {
1804 return false;
1805 }
1806
1807 if (acym_isAdmin() && acym_isAllowed('users')) {
1808 return true;
1809 }
1810
1811 $user = $this->getOneById($subscriberId);
1812 $userEmail = acym_currentUserEmail();
1813 if (!empty($user) && $user->email === $userEmail) {
1814 return true;
1815 }
1816
1817 $listClass = new ListClass();
1818 $allowedListIds = $listClass->getManageableLists();
1819 if (empty($allowedListIds)) {
1820 return false;
1821 }
1822
1823 $query = 'SELECT COUNT(*) FROM #__acym_user AS user
1824 JOIN #__acym_user_has_list AS map ON map.user_id = user.id
1825 WHERE map.list_id IN ('.implode(',', $allowedListIds).')
1826 AND user.id = '.intval($subscriberId);
1827
1828 return acym_loadResult($query) > 0;
1829 }
1830
1831 public function getXUsers(array $options = []): array
1832 {
1833 $limit = $options['limit'] ?? 10;
1834 $offset = $options['offset'] ?? 0;
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 }
1841
1842 $conditions = [];
1843 foreach ($filters as $column => $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.'%');
1848 }
1849 }
1850
1851 if (!empty($options['created_after'])) {
1852 $conditions[] = 'creation_date > '.acym_escapeDB($options['created_after']);
1853 }
1854
1855 if (!empty($options['confirmed_after'])) {
1856 $conditions[] = 'confirmation_date > '.acym_escapeDB($options['confirmed_after']);
1857 }
1858
1859 $query = 'SELECT * FROM #__acym_user';
1860 if (!empty($conditions)) {
1861 $query .= ' WHERE '.implode(' AND ', $conditions);
1862 }
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
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);
1881 }
1882 }
1883