Internal: Adapt cron scripts to Chamilo 2 with symfony

pull/5856/head
christianbeeznst 1 month ago
parent 976c59d307
commit cbff80577d
  1. 215
      public/main/cron/agenda_reminders.php
  2. 89
      public/main/cron/course_finished.php
  3. 16
      public/main/cron/notification.php
  4. 168
      public/main/cron/remind_course_expiration.php
  5. 88
      public/main/cron/request_removal_reminder.php
  6. 58
      public/main/cron/update_session_status.php
  7. 18
      public/main/inc/lib/notification.lib.php
  8. 190
      src/CoreBundle/Command/ProcessUserDataRequestsCommand.php
  9. 142
      src/CoreBundle/Command/SendCourseExpirationEmailsCommand.php
  10. 146
      src/CoreBundle/Command/SendCourseExpirationRemindersCommand.php
  11. 171
      src/CoreBundle/Command/SendEventRemindersCommand.php
  12. 62
      src/CoreBundle/Command/SendNotificationsCommand.php
  13. 100
      src/CoreBundle/Command/UpdateSessionStatusCommand.php
  14. 2
      src/CoreBundle/Entity/AgendaReminder.php
  15. 6
      src/CoreBundle/Entity/Session.php
  16. 11
      src/CoreBundle/Repository/SessionRepository.php
  17. 4
      src/CoreBundle/Resources/views/Mailer/Default/header.html.twig
  18. 2
      src/CoreBundle/Resources/views/Mailer/Legacy/cron_course_finished_body.html.twig
  19. 2
      src/CoreBundle/Resources/views/Mailer/Legacy/cron_remind_course_expiration_body.html.twig
  20. 8
      src/CoreBundle/ServiceHelper/AccessUrlHelper.php

@ -1,215 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This script send notification messages to users that have reminders from an event in their agenda.
*/
use Chamilo\CoreBundle\Entity\AgendaReminder;
use Chamilo\CoreBundle\Entity\CourseRelUser;
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
use Chamilo\CourseBundle\Entity\CCalendarEvent;
require_once __DIR__.'/../../main/inc/global.inc.php';
if ('cli' != php_sapi_name()) {
exit; //do not run from browser
}
$batchCounter = 0;
$batchSize = 100;
$now = new DateTime('now', new DateTimeZone('UTC'));
$em = Database::getManager();
$remindersRepo = $em->getRepository(AgendaReminder::class);
/** @var array<AgendaReminder> $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[] = '<p><strong>'.$event->getTitle().'</strong></p>';
if ($event->isAllDay()) {
$eventDetails[] = '<p class="small">'.get_lang('All day').'</p>';
} else {
$eventDetails[] = sprintf(
'<p class="small">'.get_lang('From %s').'</p>',
api_get_local_time($event->getStartDate(), null, null, false, true, true)
);
if (!empty($event->getEnddate())) {
$eventDetails[] = sprintf(
'<p class="small">'.get_lang('Until %s').'</p>',
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('<p><strong>%s</strong></p>', $event->getTitle()),
$event->isAllDay() ? '<p class="small">All Day</p>' : sprintf(
'<p class="small">From %s</p>',
$event->getStartDate()->format('Y-m-d H:i:s')
)
];
if ($event->getEndDate()) {
$eventDetails[] = sprintf(
'<p class="small">Until %s</p>',
$event->getEndDate()->format('Y-m-d H:i:s')
);
}
if ($event->getContent()) {
$eventDetails[] = $event->getContent();
}
if ($event->getComment()) {
$eventDetails[] = sprintf('<p class="small">%s</p>', $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();

@ -1,89 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Cron for send a email when the course are finished.
*
* @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*/
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;
}
}

@ -1,16 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
/**
* @author Julio Montoya <gugli100@gmail.com>
*/
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();

@ -1,168 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Course expiration reminder.
*
* @author Imanol Losada <imanol.losada@beeznest.com>
*/
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";
}

@ -1,88 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../inc/global.inc.php';
if (PHP_SAPI != 'cli') {
exit('Run this script through the command line or comment this line in the code');
}
$urlList = UrlManager::get_url_data();
$defaultSenderId = 1;
// Loop all portals
foreach ($urlList as $url) {
// Set access_url in order to get the correct url links and admins
$_configuration['access_url'] = $url['id'];
$sql = '';
$user_table = Database::get_main_table(TABLE_MAIN_USER);
$admin_table = Database::get_main_table(TABLE_MAIN_ADMIN);
$sql .= "SELECT u.id, v.updated_at FROM $user_table u";
// adding the filter to see the user's only of the current access_url
if (api_get_multiple_access_url()) {
$access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
$sql .= " INNER JOIN $access_url_rel_user_table url_rel_user
ON (u.id = url_rel_user.user_id)";
}
$extraFields = UserManager::createDataPrivacyExtraFields();
$extraFieldId = $extraFields['delete_legal'];
$extraFieldIdDeleteAccount = $extraFields['delete_account_extra_field'];
$extraFieldValue = Database::get_main_table(TABLE_EXTRA_FIELD_VALUES);
$sql .= " INNER JOIN $extraFieldValue v
ON (
u.id = v.item_id AND
(field_id = $extraFieldId OR field_id = $extraFieldIdDeleteAccount) AND
v.value = 1
) ";
$sql .= " WHERE 1 = 1 AND u.active <> ".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;
}

@ -1,58 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../inc/global.inc.php';
$test = true;
$em = Database::getManager();
$table = Database::get_main_table(TABLE_MAIN_SESSION);
$sql = "SELECT * FROM $table ";
$result = Database::query($sql);
$now = api_get_utc_datetime();
$line = PHP_SAPI === 'cli' ? PHP_EOL : '<br />';
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;
}

@ -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.'<br /><br />'.$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;

@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Command;
use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CoreBundle\Settings\SettingsManager;
use Database;
use DateTime;
use DateInterval;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManager;
use MessageManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
use Chamilo\CoreBundle\Entity\User;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Contracts\Translation\TranslatorInterface;
use UserManager;
class ProcessUserDataRequestsCommand extends Command
{
protected static $defaultName = 'app:process-user-data-requests';
public function __construct(
private readonly Connection $connection,
private readonly AccessUrlHelper $accessUrlHelper,
private readonly SettingsManager $settingsManager,
private readonly MailerInterface $mailer,
private readonly EntityManager $em,
private readonly TranslatorInterface $translator
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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;
}
}

@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Command;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Settings\SettingsManager;
use DateTime;
use DateTimeZone;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Twig\Environment;
use UserManager;
class SendCourseExpirationEmailsCommand extends Command
{
protected static $defaultName = 'app:send-course-expiration-emails';
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly SettingsManager $settingsManager,
private readonly MailerInterface $mailer,
private readonly Environment $twig
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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')}");
}
}
}

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Command;
use DateInterval;
use DateTime;
use DateTimeZone;
use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Chamilo\CoreBundle\Settings\SettingsManager;
class SendCourseExpirationRemindersCommand extends Command
{
protected static $defaultName = 'app:send-course-expiration-reminders';
public function __construct(
private readonly Connection $connection,
private readonly MailerInterface $mailer,
private readonly SettingsManager $settingsManager
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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');
}
}

@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Command;
use Chamilo\CoreBundle\Entity\AgendaReminder;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Settings\SettingsManager;
use Chamilo\CourseBundle\Entity\CCalendarEvent;
use Doctrine\ORM\EntityManagerInterface;
use MessageManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use DateTime;
use DateTimeZone;
class SendEventRemindersCommand extends Command
{
protected static $defaultName = 'app:send-event-reminders';
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly SettingsManager $settingsManager
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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('<p><strong>%s</strong></p>', $event->getTitle());
if ($event->isAllDay()) {
$details[] = '<p class="small">All Day</p>';
} else {
$details[] = sprintf('<p class="small">From %s</p>', $event->getStartDate()->format('Y-m-d H:i:s'));
if ($event->getEndDate()) {
$details[] = sprintf('<p class="small">Until %s</p>', $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;
}
}

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Command;
use Chamilo\CoreBundle\Framework\Container;
use Database;
use Doctrine\ORM\EntityManager;
use Notification;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class SendNotificationsCommand extends Command
{
protected static $defaultName = 'app:send-notifications';
public function __construct(
private readonly EntityManager $em
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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;
}
}

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Command;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Repository\SessionRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;
use DateTime;
class UpdateSessionStatusCommand extends Command
{
protected static $defaultName = 'app:update-session-status';
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly SessionRepository $sessionRepository
)
{
parent::__construct();
}
protected function configure(): void
{
$this
->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 : '<br />';
$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;
}
}

@ -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;

@ -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',

@ -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();
}
}

@ -3,11 +3,11 @@
<tr>
<td width="245" {{ mail_header_style }}>
<img class="navbar-brand-full" width="130"
src="{{ theme_asset('images/header-logo.png', true) }}"
src="{{ theme_asset_base64('images/header-logo.png', true) }}"
alt="Chamilo"/>
</td>
<td width="100%"> &nbsp;
</td>
</tr>
</table>
{% endautoescape %}
{% endautoescape %}

@ -1 +1 @@
<p>{{ 'MailCronCourseFinishedBody'|trans|format(complete_user_name, session_name, session_name, _s.site_name) }}</p>
<p>{{ 'MailCronCourseFinishedBody'|trans|format(complete_user_name, session_name, session_name, site_name) }}</p>

@ -1 +1 @@
<p>{{ 'MailCronCourseExpirationReminderBody'|trans|format(complete_user_name, session_name, session_access_end_date, remaining_days, _p.web, _s.institution) }}</p>
<p>{{ 'MailCronCourseExpirationReminderBody'|trans|format(complete_user_name, session_name, session_access_end_date, remaining_days, root_web, institution) }}</p>

@ -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]);

Loading…
Cancel
Save