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 +1882 -0 10.10.1 → 11.1.0 View file →
@@ -1,0 +1,1882 @@
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 +}