From cbff80577dea61eb5e4b52cf3a44474d102f24ce Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Tue, 15 Oct 2024 13:40:11 -0500 Subject: [PATCH] Internal: Adapt cron scripts to Chamilo 2 with symfony --- public/main/cron/agenda_reminders.php | 215 ------------------ public/main/cron/course_finished.php | 89 -------- public/main/cron/notification.php | 16 -- public/main/cron/remind_course_expiration.php | 168 -------------- public/main/cron/request_removal_reminder.php | 88 ------- public/main/cron/update_session_status.php | 58 ----- public/main/inc/lib/notification.lib.php | 18 +- .../ProcessUserDataRequestsCommand.php | 190 ++++++++++++++++ .../SendCourseExpirationEmailsCommand.php | 142 ++++++++++++ .../SendCourseExpirationRemindersCommand.php | 146 ++++++++++++ .../Command/SendEventRemindersCommand.php | 171 ++++++++++++++ .../Command/SendNotificationsCommand.php | 62 +++++ .../Command/UpdateSessionStatusCommand.php | 100 ++++++++ src/CoreBundle/Entity/AgendaReminder.php | 2 +- src/CoreBundle/Entity/Session.php | 6 + .../Repository/SessionRepository.php | 11 + .../views/Mailer/Default/header.html.twig | 4 +- .../cron_course_finished_body.html.twig | 2 +- ...on_remind_course_expiration_body.html.twig | 2 +- .../ServiceHelper/AccessUrlHelper.php | 8 +- 20 files changed, 849 insertions(+), 649 deletions(-) delete mode 100644 public/main/cron/agenda_reminders.php delete mode 100644 public/main/cron/course_finished.php delete mode 100644 public/main/cron/notification.php delete mode 100644 public/main/cron/remind_course_expiration.php delete mode 100644 public/main/cron/request_removal_reminder.php delete mode 100644 public/main/cron/update_session_status.php create mode 100644 src/CoreBundle/Command/ProcessUserDataRequestsCommand.php create mode 100644 src/CoreBundle/Command/SendCourseExpirationEmailsCommand.php create mode 100644 src/CoreBundle/Command/SendCourseExpirationRemindersCommand.php create mode 100644 src/CoreBundle/Command/SendEventRemindersCommand.php create mode 100644 src/CoreBundle/Command/SendNotificationsCommand.php create mode 100644 src/CoreBundle/Command/UpdateSessionStatusCommand.php diff --git a/public/main/cron/agenda_reminders.php b/public/main/cron/agenda_reminders.php deleted file mode 100644 index 8a8b6813cf..0000000000 --- a/public/main/cron/agenda_reminders.php +++ /dev/null @@ -1,215 +0,0 @@ -getRepository(AgendaReminder::class); - -/** @var array $reminders */ -$reminders = $remindersRepo->findBy(['sent' => false]); - -$senderId = (int) api_get_setting('agenda.agenda_reminders_sender_id'); - -if (empty($senderId)) { - $firstAdmin = current(UserManager::get_all_administrators()); - $senderId = $firstAdmin['user_id']; -} - -foreach ($reminders as $reminder) { - $event = $reminder->getEvent(); - - if (null === $event) { - continue; - } - - $notificationDate = clone $event->getStartDate(); - $notificationDate->sub($reminder->getDateInterval()); - - if ($notificationDate > $now) { - continue; - } - - if ('course' !== $event->determineType()) { - $eventDetails = []; - $eventDetails[] = '

'.$event->getTitle().'

'; - - if ($event->isAllDay()) { - $eventDetails[] = '

'.get_lang('All day').'

'; - } else { - $eventDetails[] = sprintf( - '

'.get_lang('From %s').'

', - api_get_local_time($event->getStartDate(), null, null, false, true, true) - ); - - if (!empty($event->getEnddate())) { - $eventDetails[] = sprintf( - '

'.get_lang('Until %s').'

', - api_get_local_time($event->getEnddate(), null, null, false, true, true) - ); - } - } - - if (!empty($event->getContent())) { - $eventDetails[] = $event->getContent(); - } - - $messageSubject = sprintf(get_lang('Reminder for event : %s'), $event->getTitle()); - $messageContent = implode(PHP_EOL, $eventDetails); - - MessageManager::send_message_simple( - $event->getResourceNode()->getCreator()->getId(), - $messageSubject, - $messageContent, - $event->getResourceNode()->getCreator()->getId() - ); - - $getInviteesForEvent = function (?CCalendarEvent $event) use ($em) { - if (!$event) { - return []; - } - - $resourceLinks = $event->getResourceNode()->getResourceLinks(); - $inviteeList = []; - foreach ($resourceLinks as $resourceLink) { - $user = $resourceLink->getUser(); - if ($user) { - $inviteeList[] = [ - 'id' => $user->getId(), - 'name' => $user->getFullname(), - ]; - } - } - - return $inviteeList; - }; - - $invitees = $getInviteesForEvent($reminder->getEvent()); - $inviteesIdList = array_column($invitees, 'id'); - foreach ($inviteesIdList as $userId) { - MessageManager::send_message_simple( - $userId, - $messageSubject, - $messageContent, - $event->getResourceNode()->getCreator()->getId() - ); - } - } else { - $eventDetails = [ - sprintf('

%s

', $event->getTitle()), - $event->isAllDay() ? '

All Day

' : sprintf( - '

From %s

', - $event->getStartDate()->format('Y-m-d H:i:s') - ) - ]; - - if ($event->getEndDate()) { - $eventDetails[] = sprintf( - '

Until %s

', - $event->getEndDate()->format('Y-m-d H:i:s') - ); - } - - if ($event->getContent()) { - $eventDetails[] = $event->getContent(); - } - - if ($event->getComment()) { - $eventDetails[] = sprintf('

%s

', $event->getComment()); - } - - $messageSubject = sprintf('Reminder: %s', $event->getTitle()); - $messageContent = implode(PHP_EOL, $eventDetails); - - $resourceLinks = $event->getResourceNode()->getResourceLinks(); - $userIdList = []; - $groupUserIdList = []; - - foreach ($resourceLinks as $resourceLink) { - if ($resourceLink->getUser()) { - $userIdList[] = $resourceLink->getUser()->getId(); - } elseif ($resourceLink->getGroup()) { - $groupUsers = GroupManager::get_users( - $resourceLink->getGroup()->getIid(), - false, - null, - null, - false, - $resourceLink->getCourse()?->getId() - ); - foreach ($groupUsers as $groupUserId) { - $groupUserIdList[] = $groupUserId; - } - } else { - $course = $resourceLink->getCourse(); - - if ($session = $resourceLink->getSession()) { - $userSubscriptions = $session->getSessionRelCourseRelUserInCourse($course)->getValues(); - - $userIdList = array_map( - fn(SessionRelCourseRelUser $sessionCourseUserSubscription) => $sessionCourseUserSubscription->getUser()->getId(), - $userSubscriptions - ); - } else { - $userSubscriptions = $course->getUsers()->getValues(); - - $userIdList = array_map( - fn(CourseRelUser $courseUserSubscription) => $courseUserSubscription->getUser()->getId(), - $userSubscriptions - ); - } - } - } - - $userIdList = array_unique($userIdList); - $groupUserIdList = array_unique($groupUserIdList); - - foreach ($userIdList as $userId) { - MessageManager::send_message_simple( - $userId, - $messageSubject, - $messageContent, - $senderId - ); - } - - foreach ($groupUserIdList as $groupUserId) { - MessageManager::send_message_simple( - $groupUserId, - $messageSubject, - $messageContent, - $senderId - ); - } - } - - $reminder->setSent(true); - - $batchCounter++; - - if (($batchCounter % $batchSize) === 0) { - $em->flush(); - } -} - -$em->flush(); -$em->clear(); diff --git a/public/main/cron/course_finished.php b/public/main/cron/course_finished.php deleted file mode 100644 index 7931c3a1da..0000000000 --- a/public/main/cron/course_finished.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ -require_once __DIR__.'/../inc/global.inc.php'; - -if ('cli' != php_sapi_name()) { - exit; //do not run from browser -} - -$isActive = 'true' === api_get_setting('cron_remind_course_expiration_activate'); - -if (!$isActive) { - exit; -} - -$endDate = new DateTime('now', new DateTimeZone('UTC')); -$endDate = $endDate->format('Y-m-d'); - -$entityManager = Database::getManager(); -$sessionRepo = $entityManager->getRepository('ChamiloCoreBundle:Session'); -$accessUrlRepo = $entityManager->getRepository('ChamiloCoreBundle:AccessUrl'); - -$sessions = $sessionRepo->createQueryBuilder('s') - ->where('s.accessEndDate LIKE :date') - ->setParameter('date', "$endDate%") - ->getQuery() - ->getResult(); - -if (empty($sessions)) { - echo "No sessions finishing today $endDate".PHP_EOL; - exit; -} - -$administrator = [ - 'complete_name' => api_get_person_name( - api_get_setting('administratorName'), - api_get_setting('administratorSurname'), - null, - PERSON_NAME_EMAIL_ADDRESS - ), - 'email' => api_get_setting('emailAdministrator'), -]; - -foreach ($sessions as $session) { - $sessionUsers = $session->getUsers(); - - if (empty($sessionUsers)) { - echo 'No users to send mail'.PHP_EOL; - exit; - } - - foreach ($sessionUsers as $sessionUser) { - $user = $sessionUser->getUser(); - - $subjectTemplate = new Template(null, false, false, false, false, false); - $subjectTemplate->assign('session_name', $session->getTitle()); - - $subjectLayout = $subjectTemplate->get_template( - 'mail/cron_course_finished_subject.tpl' - ); - - $bodyTemplate = new Template(null, false, false, false, false, false); - $bodyTemplate->assign('complete_user_name', UserManager::formatUserFullName($user)); - $bodyTemplate->assign('session_name', $session->getTitle()); - - $bodyLayout = $bodyTemplate->get_template( - 'mail/cron_course_finished_body.tpl' - ); - - api_mail_html( - UserManager::formatUserFullName($user), - $user->getEmail(), - $subjectTemplate->fetch($subjectLayout), - $bodyTemplate->fetch($bodyLayout), - $administrator['complete_name'], - $administrator['email'] - ); - - echo '============'.PHP_EOL; - echo "Email sent to: ".UserManager::formatUserFullName($user)." ({$user->getEmail()})".PHP_EOL; - echo "Session: {$session->getTitle()}".PHP_EOL; - echo "End date: {$session->getAccessEndDate()->format('Y-m-d h:i')}".PHP_EOL; - } -} diff --git a/public/main/cron/notification.php b/public/main/cron/notification.php deleted file mode 100644 index e1bec88f56..0000000000 --- a/public/main/cron/notification.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ -if (PHP_SAPI != 'cli') { - exit('Run this script through the command line or comment this line in the code'); -} - -require_once __DIR__.'/../inc/global.inc.php'; - -/** - * Notification sending. - */ -$notify = new Notification(); -$notify->send(); diff --git a/public/main/cron/remind_course_expiration.php b/public/main/cron/remind_course_expiration.php deleted file mode 100644 index 9af9c4c2a7..0000000000 --- a/public/main/cron/remind_course_expiration.php +++ /dev/null @@ -1,168 +0,0 @@ - - */ -require_once __DIR__.'/../inc/global.inc.php'; - -/** - * Initialization. - */ -if ('cli' != php_sapi_name()) { - exit; //do not run from browser -} - -$isActive = 'true' === api_get_setting('cron_remind_course_expiration_activate'); - -if (!$isActive) { - exit; -} - -$frequency = api_get_setting('cron_remind_course_expiration_frequency'); - -// Days before expiration date to send reminders -$today = gmdate("Y-m-d"); -$expirationDate = gmdate("Y-m-d", strtotime("$today + $frequency day")); - -$gradebookTable = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY); -$certificateTable = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE); -$sessionTable = Database::get_main_table(TABLE_MAIN_SESSION); -$sessionUserTable = Database::get_main_table(TABLE_MAIN_SESSION_USER); - -$query = " - SELECT DISTINCT category.session_id, certificate.user_id - FROM $gradebookTable AS category - LEFT JOIN $certificateTable AS certificate - ON category.id = certificate.cat_id - INNER JOIN $sessionTable AS session - ON category.session_id = session.id - WHERE - session.access_end_date BETWEEN '$today' AND - '$expirationDate' AND - category.session_id IS NOT NULL"; -$sessionId = 0; -$userIds = []; -$sessions = []; -$result = Database::query($query); -$urlSessionTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_SESSION); -$urlTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL); - -while ($row = Database::fetch_array($result)) { - if ($sessionId != $row['session_id']) { - $sessionId = $row['session_id']; - $userIds = []; - } - if (!is_null($row['user_id'])) { - array_push($userIds, $row['user_id']); - } - $sessions[$sessionId] = $userIds; -} - -$usersToBeReminded = []; - -foreach ($sessions as $sessionId => $userIds) { - $userId = 0; - $userIds = $userIds ? " AND sessionUser.user_id NOT IN (".implode(", ", $userIds).")" : null; - $query = " - SELECT sessionUser.session_id, sessionUser.user_id, session.name, session.access_end_date - FROM $sessionUserTable AS sessionUser - INNER JOIN $sessionTable AS session - ON sessionUser.session_id = session.id - WHERE - session_id = $sessionId$userIds"; - $result = Database::query($query); - while ($row = Database::fetch_array($result)) { - $usersToBeReminded[$row['user_id']][$row['session_id']] = [ - 'name' => $row['name'], - 'access_end_date' => $row['access_end_date'], - ]; - } -} - -if ($usersToBeReminded) { - $today = date_create($today); - $administrator = [ - 'completeName' => api_get_person_name( - api_get_setting("administratorName"), - api_get_setting("administratorSurname"), - null, - PERSON_NAME_EMAIL_ADDRESS - ), - 'email' => api_get_setting("emailAdministrator"), - ]; - echo "\n======================================================================\n\n"; - foreach ($usersToBeReminded as $userId => $sessions) { - $user = api_get_user_info($userId); - $userCompleteName = api_get_person_name( - $user['firstname'], - $user['lastname'], - null, - PERSON_NAME_EMAIL_ADDRESS - ); - foreach ($sessions as $sessionId => $session) { - $daysRemaining = date_diff($today, date_create($session['access_end_date'])); - $join = " INNER JOIN $urlSessionTable ON id = access_url_id"; - $result = Database::select( - 'url', - "$urlTable $join", - [ - 'where' => [ - 'session_id = ?' => [ - $sessionId, - ], - ], - 'limit' => '1', - ] - ); - - $subjectTemplate = new Template(null, false, false, false, false, false); - $subjectTemplate->assign('session_name', $session['name']); - $subjectTemplate->assign( - 'session_access_end_date', - $session['access_end_date'] - ); - $subjectTemplate->assign( - 'remaining_days', - $daysRemaining->format("%d") - ); - - $subjectLayout = $subjectTemplate->get_template( - 'mail/cron_remind_course_expiration_subject.tpl' - ); - - $bodyTemplate = new Template(null, false, false, false, false, false); - $bodyTemplate->assign('complete_user_name', $userCompleteName); - $bodyTemplate->assign('session_name', $session['name']); - $bodyTemplate->assign( - 'session_access_end_date', - $session['access_end_date'] - ); - $bodyTemplate->assign( - 'remaining_days', - $daysRemaining->format("%d") - ); - - $bodyLayout = $bodyTemplate->get_template( - 'mail/cron_remind_course_expiration_body.tpl' - ); - - api_mail_html( - $userCompleteName, - $user['email'], - $subjectTemplate->fetch($subjectLayout), - $bodyTemplate->fetch($bodyLayout), - $administrator['completeName'], - $administrator['email'] - ); - echo "Email sent to $userCompleteName (".$user['email'].")\n"; - echo "Session: ".$session['name']."\n"; - echo "Date end: ".$session['access_end_date']."\n"; - echo "Days remaining: ".$daysRemaining->format("%d")."\n\n"; - } - echo "======================================================================\n\n"; - } -} else { - echo "No users to be reminded\n"; -} diff --git a/public/main/cron/request_removal_reminder.php b/public/main/cron/request_removal_reminder.php deleted file mode 100644 index b2da3af7e8..0000000000 --- a/public/main/cron/request_removal_reminder.php +++ /dev/null @@ -1,88 +0,0 @@ - ".USER_SOFT_DELETED; - - if (api_get_multiple_access_url()) { - $sql .= " AND url_rel_user.access_url_id = ".api_get_current_access_url_id(); - } - - $numberOfDays = 7; - $date = new DateTime(); - $date->sub(new \DateInterval('P'.$numberOfDays.'D')); - $dateToString = $date->format('Y-m-d h:i:s'); - $sql .= " AND v.updated_at < '$dateToString'"; - - $url = api_get_path(WEB_CODE_PATH).'admin/user_list_consent.php'; - $link = Display::url($url, $url); - $subject = get_lang('A user is waiting for an action about his/her personal data request'); - - $email = api_get_configuration_value('data_protection_officer_email'); - - $message = 'Checking requests from '.strip_tags(Display::dateToStringAgoAndLongDate($dateToString))."\n"; - - $result = Database::query($sql); - while ($user = Database::fetch_assoc($result)) { - $userId = $user['id']; - $userInfo = api_get_user_info($userId); - if ($userInfo) { - $content = sprintf( - get_lang('The user %s is waiting for an action about it\'s personal data request. - - To manage personal data requests you can follow this link : %s'), - $userInfo['complete_name'], - $link - ); - - if (!empty($email)) { - api_mail_html('', $email, $subject, $content); - } else { - MessageManager::sendMessageToAllAdminUsers($defaultSenderId, $subject, $content); - } - - $date = strip_tags(Display::dateToStringAgoAndLongDate($user['updated_at'])); - $message .= "User ".$userInfo['complete_name_with_username']." is waiting for an action since $date \n"; - } - } - echo $message; -} diff --git a/public/main/cron/update_session_status.php b/public/main/cron/update_session_status.php deleted file mode 100644 index d82bf8d46d..0000000000 --- a/public/main/cron/update_session_status.php +++ /dev/null @@ -1,58 +0,0 @@ -'; -echo 'Today is : '.$now.$line; - -while ($session = Database::fetch_array($result, 'ASSOC')) { - $id = $session['id']; - $start = $session['display_start_date']; - $end = $session['display_end_date']; - //$userCount = (int) $session['nbr_users']; - $userCount = (int) SessionManager::get_users_by_session($id, 0, true); - - // 1. Si une session a lieu dans le futur, c’est à dire que la date de début est supérieur à la date du - //jour alors elle est prévue - $status = 0; - if ($start > $now) { - $status = SessionManager::STATUS_PLANNED; - } - - // 2. Si une session a plus de 2 apprenants et que la date de début est inférieur ou égale à la date - // du jour et que la date de fin n'est pas passée alors mettre le statut en cours - if ($userCount >= 2 && $start <= $now && $end > $now) { - $status = SessionManager::STATUS_PROGRESS; - } - - // 3. Si une session n’a pas d’apprenant et que la date de début est passée alors mettre le statut à - //annulée. - if ($userCount === 0 && $now > $start) { - $status = SessionManager::STATUS_CANCELLED; - } - - // 4. Si la date de fin d'une session est dépassée et qu'elle a plus de 2 apprenants alors passer le - //statut à terminée - if ($now > $end && $userCount >= 2) { - $status = SessionManager::STATUS_FINISHED; - } - - $params = [ - 'status' => $status, - ]; - if ($test != true) { - Database::update($table, $params, ['id = ?' => $id]); - } - - echo "Session #$id updated. Status = ".SessionManager::getStatusLabel($status)."($status) User count= $userCount: Start date: $start - End date: $end".$line; -} diff --git a/public/main/inc/lib/notification.lib.php b/public/main/inc/lib/notification.lib.php index b7801d32cf..9080fdf6cd 100644 --- a/public/main/inc/lib/notification.lib.php +++ b/public/main/inc/lib/notification.lib.php @@ -69,16 +69,16 @@ class Notification extends Model } } else { // Default no-reply email - $this->adminEmail = api_get_setting('noreply_email_address'); - $this->adminName = api_get_setting('siteName'); - $this->titlePrefix = '['.api_get_setting('siteName').'] '; + $this->adminEmail = api_get_setting('mail.noreply_email_address'); + $this->adminName = api_get_setting('platform.site_name'); + $this->titlePrefix = '['.api_get_setting('platform.site_name').'] '; // If no-reply email doesn't exist use the admin name/email if (empty($this->adminEmail)) { - $this->adminEmail = api_get_setting('emailAdministrator'); + $this->adminEmail = api_get_setting('admin.administrator_email'); $this->adminName = api_get_person_name( - api_get_setting('administratorName'), - api_get_setting('administratorSurname'), + api_get_setting('admin.administrator_name'), + api_get_setting('admin.administrator_surname'), null, PERSON_NAME_EMAIL_ADDRESS ); @@ -428,7 +428,7 @@ class Notification extends Model } // See message with link text - if (!empty($linkToNewMessage) && 'true' == api_get_setting('allow_message_tool')) { + if (!empty($linkToNewMessage) && 'true' == api_get_setting('message.allow_message_tool')) { $content = $content.'

'.$linkToNewMessage; } @@ -461,11 +461,11 @@ class Notification extends Model */ public static function sendPushNotification(array $userIds, $title, $content) { - if ('true' !== api_get_setting('messaging_allow_send_push_notification')) { + if ('true' !== api_get_setting('webservice.messaging_allow_send_push_notification')) { return false; } - $gdcApiKey = api_get_setting('messaging_gdc_api_key'); + $gdcApiKey = api_get_setting('webservice.messaging_gdc_api_key'); if (false === $gdcApiKey) { return false; diff --git a/src/CoreBundle/Command/ProcessUserDataRequestsCommand.php b/src/CoreBundle/Command/ProcessUserDataRequestsCommand.php new file mode 100644 index 0000000000..c26162b5a2 --- /dev/null +++ b/src/CoreBundle/Command/ProcessUserDataRequestsCommand.php @@ -0,0 +1,190 @@ +setDescription('Process user data requests for personal data actions.') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode') + ->setHelp('This command processes user data requests that require administrative action.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + Database::setManager($this->em); + + $container = $this->getApplication()->getKernel()->getContainer(); + Container::setContainer($container); + + $io = new SymfonyStyle($input, $output); + $debug = $input->getOption('debug'); + + if ($debug) { + $io->note('Debug mode activated'); + } + + $defaultSenderId = 1; + $accessUrl = $this->accessUrlHelper->getCurrent(); + $numberOfDays = 7; + $date = new DateTime(); + $date->sub(new DateInterval('P' . $numberOfDays . 'D')); + $dateToString = $date->format('Y-m-d H:i:s'); + + if ($accessUrl) { + $message = $this->processUrlData($accessUrl->getId(), $defaultSenderId, $dateToString, $io, $debug); + if ($debug) { + $io->success($message); + } + } + + return Command::SUCCESS; + } + + private function processUrlData( + int $accessUrlId, + int $defaultSenderId, + string $dateToString, + SymfonyStyle $io, + bool $debug + ): string { + + $sql = " + SELECT u.id, v.updated_at + FROM user AS u + INNER JOIN extra_field_values AS v ON u.id = v.item_id + WHERE (v.field_id IN (:deleteLegal, :deleteAccount)) + AND v.field_value = 1 + AND u.active <> :userSoftDeleted + AND v.updated_at < :dateToString + "; + + if ($this->accessUrlHelper->isMultiple()) { + $sql .= " AND EXISTS ( + SELECT 1 FROM access_url_rel_user rel + WHERE u.id = rel.user_id + AND rel.access_url_id = :accessUrlId)"; + } + + $extraFields = UserManager::createDataPrivacyExtraFields(); + $params = [ + 'deleteLegal' => $extraFields['delete_legal'], + 'deleteAccount' => $extraFields['delete_account_extra_field'], + 'userSoftDeleted' => User::SOFT_DELETED, + 'dateToString' => $dateToString, + 'accessUrlId' => $accessUrlId + ]; + + $result = $this->connection->fetchAllAssociative($sql, $params); + $usersToBeProcessed = []; + + foreach ($result as $user) { + $usersToBeProcessed[] = $user; + } + + if (empty($usersToBeProcessed)) { + return "No users waiting for data actions for Access URL ID: {$accessUrlId}"; + } + + return $this->processUsers($usersToBeProcessed, $defaultSenderId, $io, $debug); + } + + private function processUsers( + array $users, + int $defaultSenderId, + SymfonyStyle $io, + bool $debug + ): string { + + $administrator = [ + 'completeName' => $this->settingsManager->getSetting('admin.administrator_name'), + 'email' => $this->settingsManager->getSetting('admin.administrator_email'), + ]; + + $rootweb = $this->settingsManager->getSetting('platform.institution_url'); + $link = $rootweb . '/main/admin/user_list_consent.php'; + $subject = $this->translator->trans('A user is waiting for an action about his/her personal data request'); + $email = $this->settingsManager->getSetting('profile.data_protection_officer_email'); + $message = ''; + + foreach ($users as $user) { + $userId = $user['id']; + $userInfo = $this->connection->fetchAssociative("SELECT * FROM user WHERE id = ?", [$userId]); + $userInfo['complete_name'] = $userInfo['firstname'] . ' ' . $userInfo['lastname']; + $userInfo['complete_name_with_username'] = $userInfo['complete_name'].' ('.$userInfo['username'].')'; + + if (!$userInfo) { + continue; + } + + $content = $this->translator->trans( + 'The user %name% is waiting for an action about his/her personal data request. To manage personal data requests you can follow this link: %link%', + ['%name%' => $userInfo['complete_name'], '%link%' => $link] + ); + + if ($email) { + $emailMessage = (new TemplatedEmail()) + ->from($administrator['email']) + ->to($email) + ->subject($subject) + ->html($content); + + $this->mailer->send($emailMessage); + } else { + MessageManager::sendMessageToAllAdminUsers($defaultSenderId, $subject, $content); + } + + $date = (new DateTime($user['updated_at']))->format('Y-m-d H:i:s'); + $message .= sprintf( + "User %s is waiting for an action since %s \n", + $userInfo['complete_name_with_username'], + $date + ); + + if ($debug) { + $io->note("Processed user {$userInfo['complete_name']} with ID: {$userId}"); + } + } + + return $message; + } +} diff --git a/src/CoreBundle/Command/SendCourseExpirationEmailsCommand.php b/src/CoreBundle/Command/SendCourseExpirationEmailsCommand.php new file mode 100644 index 0000000000..3446a4416b --- /dev/null +++ b/src/CoreBundle/Command/SendCourseExpirationEmailsCommand.php @@ -0,0 +1,142 @@ +setDescription('Send an email to users when their course is finished.') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode') + ->setHelp('This command sends an email to users whose course session is expiring today.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $debug = $input->getOption('debug'); + $now = new DateTime('now', new DateTimeZone('UTC')); + $endDate = $now->format('Y-m-d'); + + if ($debug) { + error_log('Debug mode activated'); + $io->note('Debug mode activated'); + } + + $isActive = 'true' === $this->settingsManager->getSetting('crons.cron_remind_course_expiration_activate'); + + if (!$isActive) { + if ($debug) { + error_log('Cron job for course expiration emails is not active.'); + $io->note('Cron job for course expiration emails is not active.'); + } + return Command::SUCCESS; + } + + $sessionRepo = $this->entityManager->getRepository(Session::class); + $sessions = $sessionRepo->createQueryBuilder('s') + ->where('s.accessEndDate LIKE :date') + ->setParameter('date', "$endDate%") + ->getQuery() + ->getResult(); + + if (empty($sessions)) { + $io->success("No sessions finishing today $endDate"); + return Command::SUCCESS; + } + + $administrator = [ + 'complete_name' => $this->getAdministratorName(), + 'email' => $this->settingsManager->getSetting('admin.administrator_email'), + ]; + + foreach ($sessions as $session) { + $sessionUsers = $session->getUsers(); + + if (empty($sessionUsers)) { + $io->warning('No users to send mail for session: ' . $session->getTitle()); + continue; + } + + foreach ($sessionUsers as $sessionUser) { + $user = $sessionUser->getUser(); + $this->sendEmailToUser($user, $session, $administrator, $io, $debug); + } + } + + $io->success('Emails sent successfully for sessions expiring today.'); + return Command::SUCCESS; + } + + private function getAdministratorName(): string + { + return api_get_person_name( + $this->settingsManager->getSetting('admin.administrator_name'), + $this->settingsManager->getSetting('admin.administrator_surname'), + null, + PERSON_NAME_EMAIL_ADDRESS + ); + } + + private function sendEmailToUser(User $user, Session $session, array $administrator, SymfonyStyle $io, bool $debug): void + { + $siteName = $this->settingsManager->getSetting('platform.site_name'); + + $subject = $this->twig->render('@ChamiloCore/Mailer/Legacy/cron_course_finished_subject.html.twig', [ + 'session_name' => $session->getTitle(), + ]); + + $body = $this->twig->render('@ChamiloCore/Mailer/Legacy/cron_course_finished_body.html.twig', [ + 'complete_user_name' => UserManager::formatUserFullName($user), + 'session_name' => $session->getTitle(), + 'site_name' => $siteName, + ]); + + $email = (new Email()) + ->from($administrator['email']) + ->to($user->getEmail()) + ->subject($subject) + ->html($body); + + $this->mailer->send($email); + + if ($debug) { + error_log("Email sent to: " . UserManager::formatUserFullName($user) . " ({$user->getEmail()})"); + $io->note("Email sent to: " . UserManager::formatUserFullName($user) . " ({$user->getEmail()})"); + $io->note("Session: {$session->getTitle()}"); + $io->note("End date: {$session->getAccessEndDate()->format('Y-m-d h:i')}"); + } + } +} diff --git a/src/CoreBundle/Command/SendCourseExpirationRemindersCommand.php b/src/CoreBundle/Command/SendCourseExpirationRemindersCommand.php new file mode 100644 index 0000000000..50a9616277 --- /dev/null +++ b/src/CoreBundle/Command/SendCourseExpirationRemindersCommand.php @@ -0,0 +1,146 @@ +setDescription('Send course expiration reminders to users.') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode') + ->setHelp('This command sends email reminders to users before their course access expires.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $debug = $input->getOption('debug'); + + if ($debug) { + $io->note('Debug mode activated'); + } + + $isActive = 'true' === $this->settingsManager->getSetting('crons.cron_remind_course_expiration_activate'); + if (!$isActive) { + $io->warning('Course expiration reminder cron is not active.'); + return Command::SUCCESS; + } + + $frequency = (int) $this->settingsManager->getSetting('crons.cron_remind_course_expiration_frequency'); + $today = new DateTime('now', new DateTimeZone('UTC')); + $expirationDate = (clone $today)->add(new DateInterval("P{$frequency}D"))->format('Y-m-d'); + + $sessions = $this->getSessionsExpiringBetween($today->format('Y-m-d'), $expirationDate); + + if (empty($sessions)) { + $io->success("No users to be reminded."); + return Command::SUCCESS; + } + + foreach ($sessions as $session) { + $this->sendReminder($session, $io, $debug); + } + + $io->success('Course expiration reminders sent successfully.'); + return Command::SUCCESS; + } + + private function getSessionsExpiringBetween(string $today, string $expirationDate): array + { + $sql = " + SELECT DISTINCT category.session_id, certificate.user_id, session.access_end_date, session.title as name + FROM gradebook_category AS category + LEFT JOIN gradebook_certificate AS certificate ON category.id = certificate.cat_id + INNER JOIN session AS session ON category.session_id = session.id + WHERE session.access_end_date BETWEEN :today AND :expirationDate + AND category.session_id IS NOT NULL AND certificate.user_id IS NOT NULL + "; + + return $this->connection->fetchAllAssociative($sql, [ + 'today' => $today, + 'expirationDate' => $expirationDate + ]); + } + + + private function sendReminder(array $session, SymfonyStyle $io, bool $debug): void + { + $userInfo = $this->getUserInfo((int) $session['user_id']); + $userInfo['complete_name'] = $userInfo['firstname'] . ' ' . $userInfo['lastname']; + $remainingDays = $this->calculateRemainingDays($session['access_end_date']); + + $administrator = [ + 'completeName' => $this->settingsManager->getSetting('admin.administrator_name'), + 'email' => $this->settingsManager->getSetting('admin.administrator_email'), + ]; + + $institution = $this->settingsManager->getSetting('platform.institution'); + $rootWeb = $this->settingsManager->getSetting('platform.institution_url'); + + $email = (new TemplatedEmail()) + ->from($administrator['email']) + ->to($userInfo['email']) + ->subject('Course Expiration Reminder') + ->htmlTemplate('@ChamiloCore/Mailer/Legacy/cron_remind_course_expiration_body.html.twig') + ->context([ + 'complete_user_name' => $userInfo['complete_name'], + 'session_name' => $session['name'], + 'session_access_end_date' => $session['access_end_date'], + 'remaining_days' => $remainingDays, + 'institution' => $institution, + 'root_web' => $rootWeb, + ]); + + try { + $this->mailer->send($email); + + if ($debug) { + $io->note("Reminder sent to {$userInfo['complete_name']} ({$userInfo['email']}) for session: {$session['name']}"); + } + } catch (TransportExceptionInterface $e) { + $io->error("Failed to send reminder: {$e->getMessage()}"); + } + } + + private function getUserInfo(int $userId): array + { + $sql = "SELECT * FROM user WHERE id = :userId"; + return $this->connection->fetchAssociative($sql, ['userId' => $userId]); + } + + private function calculateRemainingDays(string $accessEndDate): string + { + $today = new DateTime('now', new DateTimeZone('UTC')); + $endDate = new DateTime($accessEndDate); + return $today->diff($endDate)->format('%d'); + } +} diff --git a/src/CoreBundle/Command/SendEventRemindersCommand.php b/src/CoreBundle/Command/SendEventRemindersCommand.php new file mode 100644 index 0000000000..13a79e5677 --- /dev/null +++ b/src/CoreBundle/Command/SendEventRemindersCommand.php @@ -0,0 +1,171 @@ +setDescription('Send notification messages to users that have reminders from events in their agenda.') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode') + ->setHelp('This command sends notifications to users who have pending reminders for calendar events.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $debug = $input->getOption('debug'); + $now = new DateTime('now', new DateTimeZone('UTC')); + + if ($debug) { + error_log('Debug mode activated'); + $io->note('Debug mode activated'); + } + + $remindersRepo = $this->entityManager->getRepository(AgendaReminder::class); + $reminders = $remindersRepo->findBy(['sent' => false]); + + $senderId = $this->settingsManager->getSetting('agenda.agenda_reminders_sender_id'); + $senderId = (int) $senderId ?: $this->getFirstAdminId(); + + $batchCounter = 0; + $batchSize = 100; + + foreach ($reminders as $reminder) { + $event = $reminder->getEvent(); + + if (!$event) { + if ($debug) { + error_log('No event found for reminder ID: ' . $reminder->getId()); + $io->note('No event found for reminder ID: ' . $reminder->getId()); + } + continue; + } + + $eventId = $event->getIid(); + $eventEntity = $this->entityManager->getRepository(CCalendarEvent::class)->find($eventId); + + if (!$eventEntity) { + if ($debug) { + error_log('No event entity found for event ID: ' . $eventId); + $io->note('No event entity found for event ID: ' . $eventId); + } + continue; + } + + $notificationDate = clone $event->getStartDate(); + $notificationDate->sub($reminder->getDateInterval()); + if ($notificationDate > $now) { + continue; + } + + $messageSubject = sprintf('Reminder for event: %s', $event->getTitle()); + $messageContent = $this->generateEventDetails($event); + $invitees = $this->getInviteesForEvent($event); + + foreach ($invitees as $userId) { + MessageManager::send_message_simple( + $userId, + $messageSubject, + $messageContent, + $senderId + ); + + if ($debug) { + error_log("Message sent to user ID: $userId for event: " . $event->getTitle()); + $io->note("Message sent to user ID: $userId for event: " . $event->getTitle()); + } + } + + $reminder->setSent(true); + $batchCounter++; + + if (($batchCounter % $batchSize) === 0) { + $this->entityManager->flush(); + + if ($debug) { + error_log('Batch of reminders flushed'); + $io->note('Batch of reminders flushed'); + } + } + } + + $this->entityManager->flush(); + if ($debug) { + error_log('Final batch of reminders flushed'); + $io->note('Final batch of reminders flushed'); + } + + $io->success('Event reminders have been sent successfully.'); + + return Command::SUCCESS; + } + + private function getFirstAdminId(): int + { + $admin = $this->entityManager->getRepository(User::class)->findOneByRole('ROLE_ADMIN'); + return $admin ? $admin->getId() : 1; + } + + private function generateEventDetails(CCalendarEvent $event): string + { + $details = []; + $details[] = sprintf('

%s

', $event->getTitle()); + + if ($event->isAllDay()) { + $details[] = '

All Day

'; + } else { + $details[] = sprintf('

From %s

', $event->getStartDate()->format('Y-m-d H:i:s')); + if ($event->getEndDate()) { + $details[] = sprintf('

Until %s

', $event->getEndDate()->format('Y-m-d H:i:s')); + } + } + + if ($event->getContent()) { + $details[] = $event->getContent(); + } + + return implode(PHP_EOL, $details); + } + + private function getInviteesForEvent(CCalendarEvent $event): array + { + $inviteeList = []; + + foreach ($event->getResourceNode()->getResourceLinks() as $resourceLink) { + if ($user = $resourceLink->getUser()) { + $inviteeList[] = $user->getId(); + } + } + + return $inviteeList; + } +} diff --git a/src/CoreBundle/Command/SendNotificationsCommand.php b/src/CoreBundle/Command/SendNotificationsCommand.php new file mode 100644 index 0000000000..de25550aa9 --- /dev/null +++ b/src/CoreBundle/Command/SendNotificationsCommand.php @@ -0,0 +1,62 @@ +setDescription('Send notifications') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode') + ->setHelp('This command sends notifications using the Notification class.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + Database::setManager($this->em); + + $container = $this->getApplication()->getKernel()->getContainer(); + Container::setContainer($container); + + $io = new SymfonyStyle($input, $output); + $debug = $input->getOption('debug'); + + if ($debug) { + error_log('Debug mode activated'); + $io->note('Debug mode activated'); + } + + $notification = new Notification(); + $notification->send(); + + if ($debug) { + error_log('Notifications have been sent.'); + $io->success('Notifications have been sent successfully.'); + } + + return Command::SUCCESS; + } +} diff --git a/src/CoreBundle/Command/UpdateSessionStatusCommand.php b/src/CoreBundle/Command/UpdateSessionStatusCommand.php new file mode 100644 index 0000000000..afe0f969a7 --- /dev/null +++ b/src/CoreBundle/Command/UpdateSessionStatusCommand.php @@ -0,0 +1,100 @@ +setDescription('Updates the status of training sessions based on their dates and user count.') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $debug = $input->getOption('debug'); + $lineBreak = PHP_SAPI === 'cli' ? PHP_EOL : '
'; + + $now = new DateTime('now', new \DateTimeZone('UTC')); + $io->text('Today is: ' . $now->format('Y-m-d H:i:s') . $lineBreak); + + $sessions = $this->sessionRepository->findAll(); + + foreach ($sessions as $session) { + $id = $session->getId(); + $start = $session->getDisplayStartDate(); + $end = $session->getDisplayEndDate(); + $userCount = $this->sessionRepository->countUsersBySession($session->getId()); + + $status = $this->determineSessionStatus($start, $end, $userCount, $now); + + if ($debug) { + $startFormatted = $start ? $start->format('Y-m-d H:i:s') : 'N/A'; + $endFormatted = $end ? $end->format('Y-m-d H:i:s') : 'N/A'; + $io->note("Session #$id: Start date: {$startFormatted} - End date: {$endFormatted}"); + } + + $session->setStatus($status); + $this->sessionRepository->update($session); + } + + if ($debug) { + $io->success('Session statuses have been updated in debug mode (changes are not saved).'); + } else { + $this->entityManager->flush(); + $io->success('Session statuses have been updated successfully.'); + } + + return Command::SUCCESS; + } + + /** + * Determines the status of a session based on its start/end dates and user count. + */ + private function determineSessionStatus(?DateTime $start, ?DateTime $end, int $userCount, DateTime $now): int + { + if ($start > $now) { + return Session::STATUS_PLANNED; + } + + if ($userCount >= 2 && $start <= $now && $end > $now) { + return Session::STATUS_PROGRESS; + } + + if ($userCount === 0 && $now > $start) { + return Session::STATUS_CANCELLED; + } + + if ($now > $end && $userCount >= 2) { + return Session::STATUS_FINISHED; + } + + return Session::STATUS_UNKNOWN; + } +} diff --git a/src/CoreBundle/Entity/AgendaReminder.php b/src/CoreBundle/Entity/AgendaReminder.php index 8e082df1ca..df06a61a33 100644 --- a/src/CoreBundle/Entity/AgendaReminder.php +++ b/src/CoreBundle/Entity/AgendaReminder.php @@ -36,7 +36,7 @@ class AgendaReminder #[Groups(['calendar_event:write', 'calendar_event:read'])] public string $period; - #[ORM\ManyToOne(inversedBy: 'reminders')] + #[ORM\ManyToOne(fetch: 'EAGER', inversedBy: 'reminders')] #[ORM\JoinColumn(referencedColumnName: 'iid', nullable: false)] private ?CCalendarEvent $event = null; diff --git a/src/CoreBundle/Entity/Session.php b/src/CoreBundle/Entity/Session.php index e6c97093af..afcb989c51 100644 --- a/src/CoreBundle/Entity/Session.php +++ b/src/CoreBundle/Entity/Session.php @@ -125,6 +125,12 @@ class Session implements ResourceWithAccessUrlInterface, Stringable public const GENERAL_COACH = 3; public const SESSION_ADMIN = 4; + public const STATUS_PLANNED = 1; + public const STATUS_PROGRESS = 2; + public const STATUS_FINISHED = 3; + public const STATUS_CANCELLED = 4; + public const STATUS_UNKNOWN = 0; + #[Groups([ 'session:basic', 'session:read', diff --git a/src/CoreBundle/Repository/SessionRepository.php b/src/CoreBundle/Repository/SessionRepository.php index d4a9595ed3..38423bdfb2 100644 --- a/src/CoreBundle/Repository/SessionRepository.php +++ b/src/CoreBundle/Repository/SessionRepository.php @@ -463,4 +463,15 @@ class SessionRepository extends ServiceEntityRepository return array_filter($sessions, $filterSessions); } + + public function countUsersBySession(int $sessionId): int + { + $qb = $this->createQueryBuilder('s'); + $qb->select('COUNT(sru.id)') + ->innerJoin('s.users', 'sru') + ->where('s.id = :sessionId') + ->setParameter('sessionId', $sessionId); + + return (int) $qb->getQuery()->getSingleScalarResult(); + } } diff --git a/src/CoreBundle/Resources/views/Mailer/Default/header.html.twig b/src/CoreBundle/Resources/views/Mailer/Default/header.html.twig index 534db190ab..9614dd07d5 100644 --- a/src/CoreBundle/Resources/views/Mailer/Default/header.html.twig +++ b/src/CoreBundle/Resources/views/Mailer/Default/header.html.twig @@ -3,11 +3,11 @@ Chamilo   -{% endautoescape %} \ No newline at end of file +{% endautoescape %} diff --git a/src/CoreBundle/Resources/views/Mailer/Legacy/cron_course_finished_body.html.twig b/src/CoreBundle/Resources/views/Mailer/Legacy/cron_course_finished_body.html.twig index 30a086c7b7..f9fca8408a 100644 --- a/src/CoreBundle/Resources/views/Mailer/Legacy/cron_course_finished_body.html.twig +++ b/src/CoreBundle/Resources/views/Mailer/Legacy/cron_course_finished_body.html.twig @@ -1 +1 @@ -

{{ 'MailCronCourseFinishedBody'|trans|format(complete_user_name, session_name, session_name, _s.site_name) }}

+

{{ 'MailCronCourseFinishedBody'|trans|format(complete_user_name, session_name, session_name, site_name) }}

diff --git a/src/CoreBundle/Resources/views/Mailer/Legacy/cron_remind_course_expiration_body.html.twig b/src/CoreBundle/Resources/views/Mailer/Legacy/cron_remind_course_expiration_body.html.twig index 4a1282b943..393054dec5 100644 --- a/src/CoreBundle/Resources/views/Mailer/Legacy/cron_remind_course_expiration_body.html.twig +++ b/src/CoreBundle/Resources/views/Mailer/Legacy/cron_remind_course_expiration_body.html.twig @@ -1 +1 @@ -

{{ 'MailCronCourseExpirationReminderBody'|trans|format(complete_user_name, session_name, session_access_end_date, remaining_days, _p.web, _s.institution) }}

+

{{ 'MailCronCourseExpirationReminderBody'|trans|format(complete_user_name, session_name, session_access_end_date, remaining_days, root_web, institution) }}

diff --git a/src/CoreBundle/ServiceHelper/AccessUrlHelper.php b/src/CoreBundle/ServiceHelper/AccessUrlHelper.php index fa2295ca96..c2793ee913 100644 --- a/src/CoreBundle/ServiceHelper/AccessUrlHelper.php +++ b/src/CoreBundle/ServiceHelper/AccessUrlHelper.php @@ -46,7 +46,13 @@ class AccessUrlHelper $accessUrl = $this->getFirstAccessUrl(); if ($this->isMultiple()) { - $url = $this->requestStack->getMainRequest()->getSchemeAndHttpHost().'/'; + $request = $this->requestStack->getMainRequest(); + + if (null === $request) { + return $accessUrl; + } + + $url = $request->getSchemeAndHttpHost().'/'; /** @var AccessUrl $accessUrl */ $accessUrl = $this->accessUrlRepository->findOneBy(['url' => $url]);