diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a124507f5fdd450403bd84115d44218d201b6d45..1e8490204358361b38352d4ec1007746821836a0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -85,6 +85,14 @@ trigger-ci-debian-bullseye: project: debian/bullseye-fusiondirectory-orchestrator-dev branch: "main" +trigger-ci-ubuntu-focal: + stage: trigger + only: + - dev + trigger: + project: ubuntu/focal-fusiondirectory-orchestrator-dev + branch: "main" + trigger-ci-centos-7: stage: trigger only: diff --git a/Changelog.md b/Changelog.md index 3c10721be219c633e7706324de63d8eefdf32bbb..8bc38857a59fc6057369294ed7a90e029551fcb1 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,35 @@ +## %"FusionDirectory Orchestrator 1.1" - 2025-01-31 + +### Added + +#### fusiondirectory-orchestrator +- fusiondirectory-orchestrator#39 [Orchestrator] - New endpoint notifications in order to manage notifications tasks +- fusiondirectory-orchestrator#43 [Orchestrator] - Create a possible plugin endpoints, allowing new endpoints developed to be considered as plugin base. +- fusiondirectory-orchestrator#48 [Orchestrator] - Add options mailAuth and mail_sec_verify in orchestrator configuration file +- fusiondirectory-orchestrator#52 [Orchestrator] - AUDIT - automatic deletion from audit tasks +- fusiondirectory-orchestrator#57 [Orchestrator] - New endpoint for userReminder allowing to send notification to use with token to extend their account longevity +- fusiondirectory-orchestrator#62 [Orchestrator] - lifeCycle prolongation adds time from date of process and not from resource end date +- fusiondirectory-orchestrator#64 [Orchestrator] - Reminder - Removal of fdTasksReminderManager +- fusiondirectory-orchestrator#65 [Orchestrator] - Format of the orchestrator.conf updates + +### Changed + +#### fusiondirectory-orchestrator +- fusiondirectory-orchestrator#55 [Orchestrator] - Notifications must be updated to be aware of supannStatus values + +### Removed + +#### fusiondirectory-orchestrator +- fusiondirectory-orchestrator#37 [Orchestrator] - Analyze the library mail to integrate it to integrator + +### Fixed + +#### fusiondirectory-orchestrator +- fusiondirectory-orchestrator#46 [Orchestrator] - taskGateway verify schedule strtotime issues +- fusiondirectory-orchestrator#47 Use overload instead of load for dotenv +- fusiondirectory-orchestrator#61 [Orchestrator] - LifeCycle array supann is analyzed with static numbering +- fusiondirectory-orchestrator#66 [Orchestrator] - user-reminder - issue when only one members is set + ## %"FusionDirectory Orchestrator 1.0" - 2024-04-05 ### Added diff --git a/README.md b/README.md index 0de72f81e8c734f8168207f4ea022db2545f2cb5..f536abfae30bba52acbc1b572306929c66ef3b30 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,48 @@ # FusionDirectory Orchestrator -FusionDirectory Orchestrator is a REST API orchestrator. -It is a part of our new workflow management system defined in FusionDirectory -Is purpose is to execute tasks defined in FusionDirectory. +FusionDirectory Orchestrator is a RESTful web service using JWT authentication, designed to manage and execute tasks efficiently. + +It supports multiple endpoints with plugin integration for custom processing or specialized tasks. + +Tasks are defined within FusionDirectory, with a client available to query endpoints and manage workflows. + +Common tasks include account lifecycle, notifications, reminders, mail automation, audit log management, and more. ## Features * Tasks management. * Tasks execution. +* Workflow management. * JWT Authentication methods ## Tasks management -FusionDirectory Orchestrator REST API allows the retrieval of existing created tasks. -It offers a simple view on the status of each task. +FusionDirectory Orchestrator REST API provides seamless management and retrieval of tasks created within FusionDirectory. +It offers a clear and concise view of the status of each task, including subtasks, allowing for detailed tracking and reporting. -It is possible to retrieve specialized tasks and see their content. +With its extensible design, the Orchestrator supports specialized tasks such as mail automation, notifications, reminders, +account lifecycle management, and audit log processing, enabling tailored workflows to meet diverse needs ## Tasks execution -One of the main aspects of FusionDirectory Orchestrator is to allow processing of some desired tasks. +One of the core functionalities of the **FusionDirectory Orchestrator** is the execution and processing of various tasks as defined within FusionDirectory. + +- **Mail Tasks**: + When triggered, tasks of type "Mail" will automatically send the relevant emails if the scheduled conditions are met, ensuring timely communication. + +- **Life Cycle Tasks**: + These tasks are responsible for updating specialized attributes, such as *supann* attributes, in accordance with defined lifecycle processes. + +- **Notification Tasks**: + When attributes are modified, "Notification" tasks will send automated email alerts to keep users informed of changes. + +- **Reminder Tasks**: + These tasks send reminders to users, potentially including links to extend or prolong their account, ensuring critical actions are not missed. -For example : +- **Audit Tasks**: + Tasks of this type allow for the management of audit logs, including the deletion of logs based on configurable retention policies, ensuring compliance and data management. -* In case of a task of type "Mail", the list of related emails will be sent if scheduled is matched. -* In case of a task of type "Life Cycle", the specialized supann attributes will be updated accordingly. +The **Orchestrator client** provides a user-friendly interface to activate and manage these tasks, allowing for seamless workflow execution and efficient task orchestration across the system. ## Get help @@ -36,12 +54,10 @@ There are a couple of ways you can try [to get help][get help]. Professional support is provided through of subscription. -We have two type of subscription : +* [FusionDirectory Subscription][subscription-fusiondirectory] : Global subscription for FusionDirectory -* [FusionDirectory][subscription-fusiondirectory] : Global subscription for FusionDirectory and all the plugins -* [FusionDirectory Plus][subscription-fusiondirectory-plus] : Expert Support on Education, Deployment and Infrastructure plugins - -The subscription provides access to FusionDirectory's stable enterprise repository, providing reliable software updates and security enhancements, as well as technical help and support. +The subscription provides access to FusionDirectory's enterprise repository, tested and pre-packaged versions with patches between versions, +providing reliable software updates and security enhancements, as well as technical help and support. Choose the plan that's right for you. Our subscriptions are flexible and scalable according to your needs @@ -59,9 +75,7 @@ If you like us and want to send us a small contribution, you can use the followi * [donate-kofi] -* [donate-opencollective] - -* [donate-communitybridge] +* [donate-github] ## License @@ -69,13 +83,11 @@ If you like us and want to send us a small contribution, you can use the followi [FusionDirectory]: https://www.fusiondirectory.org/ -[fusiondirectory-install]: https://fusiondirectory-user-manual.readthedocs.io/en/1.4/fusiondirectory/install/index.html +[fusiondirectory-install]: https://fusiondirectory-user-manual.readthedocs.io/en/latest/fusiondirectory/install/index.html [get help]: https://fusiondirectory-user-manual.readthedocs.io/en/latest/support/index.html -[subscription-fusiondirectory]: https://www.fusiondirectory.org/en/subscription-fusiondirectory/ - -[subscription-fusiondirectory-plus]: https://www.fusiondirectory.org/en/subscriptions-fusiondirectory-plus/ +[subscription-fusiondirectory]: https://www.fusiondirectory.org/en/iam-tool-service-subscriptions/ [register]: https://register.fusiondirectory.org @@ -83,9 +95,5 @@ If you like us and want to send us a small contribution, you can use the followi [donate-kofi]: https://ko-fi.com/fusiondirectory -[donate-opencollective]: https://opencollective.com/fusiondirectory - -[donate-communitybridge]: https://funding.communitybridge.org/projects/fusiondirectory - - +[donate-github]: https://github.com/fusiondirectory diff --git a/api/index.php b/api/index.php index 13839e87f0e2e2915335912a96106bd27d44ef9e..2bea3a3e8cc82b81313e89fd9eeebb7f5f865683 100644 --- a/api/index.php +++ b/api/index.php @@ -3,16 +3,20 @@ declare(strict_types=1); require __DIR__ . "/../config/bootstrap.php"; -$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH); - +// Parsing of the URI received as WEB request. +$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH); $parts = explode("/", $path); - // We only need the name of the resource $resource = $parts[3]; // And the tasks object required Ex: http://orchestrator/api/task(3)/object(4)/ // Example : mail is an object type of tasks $object_type = $parts[4] ?? NULL; +// Parsing of the raw data potentially passed as json REST data to the API +$rawBody = file_get_contents('php://input'); +// Decode the JSON data and set to null if no body received +$jsonBody = !empty ($rawBody) ? json_decode($rawBody, TRUE) : NULL; + switch ($resource) { case "login" : @@ -39,6 +43,12 @@ switch ($resource) { // Retrieve an authenticated ldap connection $ldap_connect = new Ldap($_ENV["FD_LDAP_MASTER_URL"], $_ENV["LDAP_ADMIN"], $_ENV["LDAP_PWD"]); +// Set timezone according to what's referenced in FusionDirectory configuration +$timezone = $ldap_connect->searchInLdap($ldap_connect->getConnection(), + '(objectClass=FusionDirectoryConf)', ['fdTimezone'], "cn=config,ou=fusiondirectory,".$_ENV["LDAP_BASE"]); +// Set default timezone retrieved. +date_default_timezone_set($timezone[0]['fdtimezone'][0]); + // Retrieve all user info based on the dsa common name (CN). $user_gateway = new UserGateway($ldap_connect); @@ -60,4 +70,4 @@ $task_gateway = new TaskGateway($ldap_connect); $controller = new TaskController($task_gateway); // Process Request Passing Resources Attributes Values ($id) -$controller->processRequest($_SERVER['REQUEST_METHOD'], $object_type); +$controller->processRequest($_SERVER['REQUEST_METHOD'], $object_type, $jsonBody); diff --git a/config/bootstrap.php b/config/bootstrap.php index baf9d5b32528418d9bb550d37c5b11c7016a3d14..e225c5ba50688b4b81a61112649991c1c82e57bf 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -8,21 +8,23 @@ function autoload ($class) { // Integrator is required - require '/usr/share/php/FusionDirectory/autoloader.php'; + require_once '/usr/share/php/FusionDirectory/autoloader.php'; + // avoid error handler requirements error, as it should be one of the first to load ? + require_once dirname(__DIR__).'/library/ErrorHandler.php'; if (strpos($class, 'PHPMailer') !== FALSE) { - require("/usr/share/php/libphp-phpmailer/src/Exception.php"); - require("/usr/share/php/libphp-phpmailer/src/PHPMailer.php"); - require("/usr/share/php/libphp-phpmailer/src/SMTP.php"); + require_once("/usr/share/php/libphp-phpmailer/src/Exception.php"); + require_once("/usr/share/php/libphp-phpmailer/src/PHPMailer.php"); + require_once("/usr/share/php/libphp-phpmailer/src/SMTP.php"); } - $relative_class = str_replace('\\', '/', $class) . '.php'; - $base_dirs = ['/usr/share/php', dirname(__DIR__)]; - $files = []; + $relative_class = str_replace('\\', '/', $class) . '.php'; + $base_dirs = ['/usr/share/php', dirname(__DIR__)]; + $files = []; foreach ($base_dirs as $base_dir) { - $dir = new RecursiveDirectoryIterator($base_dir); - $iter = new RecursiveIteratorIterator($dir); - $regex = new RegexIterator($iter, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); + $dir = new RecursiveDirectoryIterator($base_dir); + $iter = new RecursiveIteratorIterator($dir); + $regex = new RegexIterator($iter, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); foreach ($regex as $file) { $files[] = $file[0]; @@ -30,12 +32,12 @@ function autoload ($class) } foreach ($files as $file) { - if (strpos($file, $relative_class) !== FALSE) { - - require $file; + if (stripos(strtolower($file), strtolower($relative_class)) !== FALSE) { + require_once $file; break; } } + } spl_autoload_register('autoload'); @@ -49,6 +51,6 @@ set_error_handler(static function (int $errno, string $errstr, string $errfile, set_exception_handler("ErrorHandler::handleException"); $dotenv = Dotenv\Dotenv::create('/etc/fusiondirectory-orchestrator', 'orchestrator.conf'); -$dotenv->load(); +$dotenv->overload(); header("Content-type: application/json; charset=UTF-8"); diff --git a/contrib/orchestrator.conf b/contrib/orchestrator.conf index fb017e861e3fc6de0d2c606c0e10472431f42ad8..98445c1abdd1faa07785f326fac77302e0a83eaf 100644 --- a/contrib/orchestrator.conf +++ b/contrib/orchestrator.conf @@ -4,11 +4,23 @@ #Information related to LDAP FD_LDAP_MASTER_URL="ldaps://ldap.fusiondirectory.org" +LDAP_BASE="dc=fusiondirectory,dc=org" LDAP_ADMIN="cn=admin,dc=fusiondirectory,dc=org" LDAP_PWD="<ldap_pass>" + +#Information related to the DSA login. Required by the fusiondirectory-orchestrator-client. #Only DSA authentication is allowed LDAP_OU_DSA="ou=dsa,dc=fusiondirectory,dc=org" -LDAP_BASE="dc=fusiondirectory,dc=org" +DSA_LOGIN="<dsa_login>" +DSA_PASS="<dsa_pass>" + +#Information related to the webservice of FusionDirectory. Required to call FusionDirectory webservice. +FUSION_DIRECTORY_API_URL="https://fd.fusiondirectory.org/rest.php/v1" +WEB_LOGIN="weblogin_username" +WEB_PASS="weblogin_password" + +#Information related to the FusionDirectory Orchestrator Endpoint used by the client +ORCHESTRATOR_API_URL="https://fd.fusiondirectory.org/orchestrator" #Information related to Token management #SECRET_KEY a 256 bit WEB keys required @@ -18,18 +30,12 @@ TOKEN_EXPIRY="300" REFRESH_EXPIRY="432000" #Information related to PHP Mailer +MAIL_AUTH="TRUE" +# If mail auth is TRUE, below user and pass are required MAIL_USER="fusiondirectory" MAIL_PASS="<mail_pass>" MAIL_HOST="mail.fusiondirectory.org" +MAIL_SEC_VERIFY="TRUE" +# If mail sec verify is set to true, mail_sec is required MAIL_SEC="<ssl/tls>" MAIL_PORT="25" - -#Information related to the DSA login. Required by the fusiondirectory-orchestrator-client. -DSA_LOGIN="<dsa_login>" -DSA_PASS="<dsa_pass>" -ORCHESTRATOR_API_URL="https://fd.fusiondirectory.org/orchestrator" - -#Information related to the webservice library of Orchestrator. Required to call FD webservice. -FUSION_DIRECTORY_API_URL="https://fd.fusiondirectory.org/rest.php/v1" -WEB_LOGIN="weblogin_username" -WEB_PASS="weblogin_password" \ No newline at end of file diff --git a/library/Ldap.php b/library/Ldap.php index 1565d6509de1350e8a768001ba2e8d55f05cf91e..27fbb72dd4dee4eabae4a43628798175c13a7219 100644 --- a/library/Ldap.php +++ b/library/Ldap.php @@ -32,4 +32,36 @@ class Ldap return $ds; } + + /** + * @param $ds + * @param string $filter + * @param array $attrs + * @param string|NULL $dn + * @return array + * Note : A generic method allowing to search in LDAP. + */ + public function searchInLdap ($ds, string $filter = '', array $attrs = [], string $dn = NULL): array + { + $result = []; + + if (empty($dn)) { + $dn = $_ENV["LDAP_BASE"]; + } + + try { + $sr = ldap_search($ds, $dn, $filter, $attrs); + $info = ldap_get_entries($ds, $sr); + } catch (Exception $e) { + // build array for return response + $result = [json_encode(["Ldap Error" => "$e"])]; // string returned + } + + // Verify if the above ldap search succeeded. + if (!empty($info) && is_array($info) && $info["count"] >= 1) { + return $info; + } + + return $result; + } } \ No newline at end of file diff --git a/library/MailController.php b/library/MailController.php deleted file mode 100644 index cec945bc4b7ae6be4290b7e458b05ba81c0c614c..0000000000000000000000000000000000000000 --- a/library/MailController.php +++ /dev/null @@ -1,111 +0,0 @@ -<?php - -use PHPMailer\PHPMailer\PHPMailer; -use PHPMailer\PHPMailer\SMTP; -use PHPMailer\PHPMailer\Exception; - -class MailController -{ - - protected string $setFrom; - protected ?string $setBCC; - protected array $recipients; - protected string $body; - protected ?string $signature; - protected string $subject; - protected ?bool $receipt; - protected ?array $attachments; - private PHPMailer $mail; - - function __construct ( - string $setFrom, - ?string $setBCC, - array $recipients, - string $body, - ?string $signature, - string $subject, - bool $receipt = NULL, - array $attachments = NULL - ) - { - // The TRUE value passed it to enable the exception handling properly. - $this->mail = new PHPMailer(TRUE); - $this->setFrom = $setFrom; - $this->setBCC = $setBCC; - $this->recipients = $recipients; - $this->body = $body; - $this->signature = $signature; - $this->subject = $subject; - $this->receipt = $receipt; - $this->attachments = $attachments; - - } - - public function sendMail (): array - { - $this->mail->isSMTP(); - $this->mail->Host = $_ENV["MAIL_HOST"]; - - /* - * In case there are FQDN errors responses by the SMTP server, try below. - * $this->mail->Helo = '['.$_SERVER['SERVER_ADDR'].']'; - */ - - $this->mail->SMTPAuth = TRUE; - $this->mail->Username = $_ENV["MAIL_USER"]; - $this->mail->Password = $_ENV["MAIL_PASS"]; - $this->mail->SMTPSecure = $_ENV["MAIL_SEC"]; - $this->mail->Port = $_ENV["MAIL_PORT"]; - $this->mail->AuthType = 'LOGIN'; - - if (!empty($this->attachments)) { - foreach ($this->attachments as $attachment) { - $this->mail->addStringAttachment($attachment['content'], $attachment['cn']); - } - } - - $this->mail->setFrom($this->setFrom); - - if (!empty($this->setBCC)) { - $this->mail->addBCC($this->setBCC); - } - - if (!empty($this->receipt)) { - $this->mail->addCustomHeader('Disposition-Notification-To', $this->setFrom); - } - $this->mail->Subject = $this->subject; - $this->mail->Body = $this->body; - - if (!empty($this->signature)) { - $this->mail->Body .= "\n\n" . $this->signature; - } - - // add it to keep SMTP connection open after each email sent - $this->mail->SMTPKeepAlive = TRUE; - - unset($this->recipients["count"]); - - // Our returned array - $errors = []; - - foreach ($this->recipients as $mail) { - $this->mail->addAddress($mail); - - try { - $this->mail->send(); - - } catch (Exception $e) { - $errors[] = $this->mail->ErrorInfo; - - } - $this->mail->clearAddresses(); - - if (empty($errors)) { - $errors[] = "SUCCESS"; - } - } - - $this->mail->smtpClose(); - return $errors; - } -} \ No newline at end of file diff --git a/library/TaskController.php b/library/TaskController.php index af549d86c9cb913d9d82a85c89368a6a6a4cb3f9..849f5b65c2971b92097a9098e764bcca0987b2cb 100644 --- a/library/TaskController.php +++ b/library/TaskController.php @@ -1,5 +1,11 @@ <?php +/* + * The controller is part of the presentation layer in an MVC (Model-View-Controller) architecture. + * Its primary role is to handle incoming HTTP requests, process user inputs, and determine which business logic (services or models) to invoke. + * It then decides what response to send back to the client. + */ + class TaskController { private TaskGateway $gateway; @@ -9,54 +15,74 @@ class TaskController $this->gateway = $gateway; } - public function processRequest (string $method, ?string $object_type): void + protected function parseJsonResult ($result = NULL): void + { + if (!empty($result)) { + echo json_encode($result, JSON_PRETTY_PRINT); + } else { + // No result received + echo json_encode("No results received from the endpoint, maybe nothing to be processed ?"); + } + } + + /** + * @param string $method + * @param string|null $objectType + * @param $jsonBody + * @return void + * @throws Exception + * NOTE : objectType is actually the task type. + */ + public function processRequest (string $method, ?string $objectType, $jsonBody = NULL): void { + // Allow result to be nullable. + $result = NULL; + // If no specific tasks object specified, return all tasks - if ($object_type === NULL) { + if ($objectType == NULL) { if ($method == "GET") { echo json_encode($this->gateway->getTask(NULL)); } else { $this->respondMethodAllowed("GET"); } - - // Otherwise return the tasks object specified + // Otherwise continue the process of the specific task / object type specified } else { - $task = $this->gateway->getTask($object_type); - if (!$task) { - - $this->respondNotFound($object_type); - return; - } - + // Define an empty array as returning result. switch ($method) { + // GET methods case "GET": - echo json_encode($task); + switch ($objectType) { + case $objectType: + if (class_exists($objectType)) { + $endpoint = new $objectType; + $result = $endpoint->processEndPointGet(); + } + break; + + default: + $this->respondMethodAllowed("GET, PATCH, DELETE"); + } + $this->parseJsonResult($result); break; + // PATCH methods case "PATCH": - switch ($object_type) { - case "mail": - $result = $this->gateway->processMailTasks($task); - break; - case 'lifeCycle': - $result = $this->gateway->processLifeCycleTasks($task); - break; + switch ($objectType) { case 'removeSubTasks': $result = $this->gateway->removeCompletedTasks(); break; case 'activateCyclicTasks': $result = $this->gateway->activateCyclicTasks(); break; + case $objectType: + if (class_exists($objectType)) { + $endpoint = new $objectType($this->gateway); + $result = $endpoint->processEndPointPatch($jsonBody); + } + break; } - if (!empty($result)) { - echo json_encode($result, JSON_PRETTY_PRINT); - - } else { - // To be modified and enhance, no results does not always mean no emails in current logic. - echo json_encode("No emails were sent."); - } - + $this->parseJsonResult($result); break; case "DELETE": @@ -74,11 +100,11 @@ class TaskController header("Allow: $allowed_methods"); } - private function respondNotFound (string $object_type): void + public static function respondNotFound (string $objectType): void { http_response_code(404); // Task ID is easier to be used - requires unique ID attributes during task creation (FD-Interface) - echo json_encode(["message" => "Task object type : $object_type not found"]); + echo json_encode(["message" => "Task object type : $objectType not found"]); } } \ No newline at end of file diff --git a/library/TaskGateway.php b/library/TaskGateway.php index d8462861bb1de4c6aa3e139394982708562dcd46..823872e3dcb9a45aa3e9d995a0a9b8f3ccd4bce5 100644 --- a/library/TaskGateway.php +++ b/library/TaskGateway.php @@ -2,10 +2,16 @@ /** * Note : Tasks engine for FusionDirectory. + * The gateway, often known as a data gateway or data access layer, is responsible for abstracting and encapsulating the interaction with an external system or a data source. + * (e.g., an LDAP, an API, or another service). + * It provides a unified interface for these operations. */ class TaskGateway { - private $ds; + /** + * @var resource|null + */ + public $ds; // Variable type can be LDAP : enhancement public function __construct ($ldap_connect) @@ -21,14 +27,20 @@ class TaskGateway public function getTask (?string $object_type): array { switch ($object_type) { - case "mail": - $list_tasks = $this->getLdapTasks("(&(objectClass=fdTasksGranular)(fdtasksgranulartype=Mail Object))"); - unset($list_tasks["count"]); - break; case "lifeCycle": $list_tasks = $this->getLdapTasks("(&(objectClass=fdTasksGranular)(fdtasksgranulartype=Life Cycle))"); - unset($list_tasks["count"]); + $this->unsetCountKeys($list_tasks); + break; + + case "notifications": + $list_tasks = $this->getLdapTasks("(&(objectClass=fdTasksGranular)(fdtasksgranulartype=Notifications))"); + $this->unsetCountKeys($list_tasks); + break; + + case "reminder": + $list_tasks = $this->getLdapTasks("(&(objectClass=fdTasksGranular)(fdtasksgranulartype=Reminder))"); + $this->unsetCountKeys($list_tasks); break; case "removeSubTasks": @@ -42,6 +54,11 @@ class TaskGateway $list_tasks = $this->getLdapTasks("(objectClass=fdTasks)", ["cn", "objectClass"]); break; + case $object_type: + $list_tasks = $this->getLdapTasks("(&(objectClass=fdTasksGranular)(fdtasksgranulartype=" . $object_type . "))"); + $this->unsetCountKeys($list_tasks); + break; + //Will match any object type passed not found. default: // return empty array which will be interpreted as FALSE by parent. @@ -53,205 +70,37 @@ class TaskGateway } /** - * @param array $list_tasks - * @return array + * @param array $task + * @return bool + * @throws Exception */ - public function processMailTasks (array $list_tasks): array + public function statusAndScheduleCheck (array $task): bool { - $result = []; - - $fdTasksConf = $this->getLdapTasks( - "(objectClass=fdTasksConf)", - ["fdTasksConfLastExecTime", "fdTasksConfIntervalEmails", "fdTasksConfMaxEmails"] - ); - - // set the maximum mails to be sent to the configured value or 50 if not set. - $maxMailsConfig = $fdTasksConf[0]["fdtasksconfmaxemails"][0] ?? 50; - - if ($this->verifySpamProtection($fdTasksConf)) { - foreach ($list_tasks as $mail) { - - $maxMailsIncrement = 0; - - // verify status before processing (to be checked with schedule as well). - if ($mail["fdtasksgranularstatus"][0] == 1 && $this->verifySchedule($mail["fdtasksgranularschedule"][0])) { - - // Search for the related attached mail object. - $cn = $mail["fdtasksgranularref"][0]; - $mailInfos = $this->getLdapTasks("(|(objectClass=fdMailTemplate)(objectClass=fdMailAttachments))", [], $cn); - $mailContent = $mailInfos[0]; - - // Only takes arrays related to files attachments for the mail template selected - unset($mailInfos[0]); - // Re-order keys - unset($mailInfos['count']); - $mailAttachments = array_values($mailInfos); - - $setFrom = $mail["fdtasksgranularmailfrom"][0]; - $setBCC = $mail["fdtasksgranularmailbcc"][0] ?? NULL; - $recipients = $mail["fdtasksgranularmail"]; - $body = $mailContent["fdmailtemplatebody"][0]; - $signature = $mailContent["fdmailtemplatesignature"][0] ?? NULL; - $subject = $mailContent["fdmailtemplatesubject"][0]; - $receipt = $mailContent["fdmailtemplatereadreceipt"][0]; - - foreach ($mailAttachments as $file) { - $fileInfo['cn'] = $file['cn'][0]; - $fileInfo['content'] = $file['fdmailattachmentscontent'][0]; - $attachments[] = $fileInfo; - } - - // Required before passing the array to the constructor mail. - if (empty($attachments)) { - $attachments = NULL; - } - - $mail_controller = new MailController($setFrom, - $setBCC, - $recipients, - $body, - $signature, - $subject, - $receipt, - $attachments); - - $mailSentResult = $mail_controller->sendMail(); - - if ($mailSentResult[0] == "SUCCESS") { - - // The third arguments "2" is the status code of success for mail as of now 18/11/22 - $result[$mail["dn"]]['statusUpdate'] = $this->updateTaskStatus($mail["dn"], $mail["cn"][0], "2"); - $result[$mail["dn"]]['mailStatus'] = 'mail : ' . $mail["dn"] . ' was successfully sent'; - $result[$mail["dn"]]['updateLastExec'] = $this->updateLastMailExecTime($fdTasksConf[0]["dn"]); - - } else { - $result[$mail["dn"]]['statusUpdate'] = $this->updateTaskStatus($mail["dn"], $mail["cn"][0], $mailSentResult[0]); - $result[$mail["dn"]] = $mailSentResult; - } - - // Verification anti-spam max mails to be sent and quit loop if matched - $maxMailsIncrement += 1; - if ($maxMailsIncrement == $maxMailsConfig) { - break; - } - - } - } - } - - return $result; + return $task["fdtasksgranularstatus"][0] == 1 && $this->verifySchedule($task["fdtasksgranularschedule"][0]); } /** - * @param array $list_tasks - * @return array - * Note : Verify the status and schedule as well as searching for the correct life cycle behavior from main task. + * @param $array + * @return void + * Simple take an array as referenced and loop to remove all key having count */ - public function processLifeCycleTasks (array $list_tasks): array + public function unsetCountKeys (&$array) { - // Array representing the status of the subtask. - $result = []; - // Initiate the object webservice. - $webservice = new WebServiceCall($_ENV['FUSION_DIRECTORY_API_URL'] . '/login', 'POST'); - // Required to prepare future webservice call. E.g. Retrieval of mandatory token. - $webservice->setCurlSettings(); - - foreach ($list_tasks as $task) { - // If the tasks must be treated - status and scheduled - process the sub-tasks - if ($task["fdtasksgranularstatus"][0] == 1 && $this->verifySchedule($task["fdtasksgranularschedule"][0])) { - - // Simply retrieve the lifeCycle behavior from the main related tasks, sending the dns and desired attributes - $lifeCycleBehavior = $this->getLdapTasks('(objectClass=*)', ['fdTasksLifeCyclePreResource', - 'fdTasksLifeCyclePreState', 'fdTasksLifeCyclePreSubState', - 'fdTasksLifeCyclePostResource', 'fdTasksLifeCyclePostState', 'fdTasksLifeCyclePostSubState', 'fdTasksLifeCyclePostEndDate'], - '', $task['fdtasksgranularmaster'][0]); - - // Simply retrieve the current supannStatus of the user DN related to the task at hand. - $currentUserLifeCycle = $this->getLdapTasks('(objectClass=supannPerson)', ['supannRessourceEtatDate'], - '', $task['fdtasksgranulardn'][0]); - - // Compare both the required schedule and the current user status - returning TRUE if modification is required. - if ($this->isLifeCycleRequiringModification($lifeCycleBehavior, $currentUserLifeCycle)) { - - // This will call a method to modify the ressourcesSupannEtatDate of the DN linked to the subTask - $lifeCycleResult = $this->updateLifeCycle($lifeCycleBehavior, $task['fdtasksgranulardn'][0]); - if ($lifeCycleResult === TRUE) { - - $result[$task['dn']]['results'] = json_encode("Account states have been successfully modified for " . $task['fdtasksgranulardn'][0]); - // Status of the task must be updated to success - $updateResult = $this->updateTaskStatus($task['dn'], $task['cn'][0], '2'); - - // Here the user is refresh in order to activate methods based on supann Status changes. - $result[$task['dn']]['refreshUser'] = $webservice->refreshUserInfo($task['fdtasksgranulardn'][0]); - - // In case the modification failed - } else { - $result[$task['dn']]['results'] = json_encode("Error updating " . $task['fdtasksgranulardn'][0] . "-" . $lifeCycleResult); - // Update of the task status error message - $updateResult = $this->updateTaskStatus($task['dn'], $task['cn'][0], $lifeCycleResult); - } - // Verification if the sub-task status has been updated correctly - if ($updateResult === TRUE) { - $result[$task['dn']]['statusUpdate'] = 'Success'; - } else { - $result[$task['dn']]['statusUpdate'] = $updateResult; - } - // Remove the subtask has it is not required to update it nor to process it. - } else { - $result[$task['dn']]['results'] = 'Sub-task removed for : ' . $task['fdtasksgranulardn'][0] . ' with result : ' - . $this->removeSubTask($task['dn']); - $result[$task['dn']]['statusUpdate'] = 'No updates required, sub-task will be removed.'; - } + foreach ($array as $key => &$value) { + if (is_array($value)) { + $this->unsetCountKeys($value); + } elseif ($key === 'count') { + unset($array[$key]); } } - // If array is empty, no tasks of type life cycle needs to be treated. - if (empty($result)) { - $result = 'No tasks of type "Life Cycle" requires processing.'; - } - return [$result]; - } - - /** - * @param array $lifeCycleBehavior - * @param string $userDN - * @return bool|string - */ - protected function updateLifeCycle (array $lifeCycleBehavior, string $userDN) - { - // Extracting values of desired post-state behavior - $newEntry['Resource'] = $lifeCycleBehavior[0]['fdtaskslifecyclepostresource'][0]; - $newEntry['State'] = $lifeCycleBehavior[0]['fdtaskslifecyclepoststate'][0]; - $newEntry['SubState'] = $lifeCycleBehavior[0]['fdtaskslifecyclepostsubstate'][0] ?? ''; //SubState is optional - $newEntry['EndDate'] = $lifeCycleBehavior[0]['fdtaskslifecyclepostenddate'][0] ?? 0; //EndDate is optional - - // Require the date of today to update the start of the new resources (If change of status). - $currentDate = new DateTime(); - // Date of today + numbers of days to add for end date. - $newEndDate = new DateTime(); - $newEndDate->modify("+" . $newEntry['EndDate'] . " days"); - - // Prepare the ldap entry to be modified - $ldapEntry = []; - $ldapEntry['supannRessourceEtatDate'] = "{" . $newEntry['Resource'] . "}" - . $newEntry['State'] . ":" - . $newEntry['SubState'] . ":" . - $currentDate->format('Ymd') . ":" - . $newEndDate->format('Ymd'); - - try { - $result = ldap_modify($this->ds, $userDN, $ldapEntry); - } catch (Exception $e) { - $result = json_encode(["Ldap Error" => "$e"]); - } - - return $result; + unset($value); //unset the reference after the loop for security best practise. } /** * @param bool|string $subTaskDn * @return bool|string */ - protected function removeSubTask ($subTaskDn) + public function removeSubTask ($subTaskDn) { try { $result = ldap_delete($this->ds, $subTaskDn); @@ -274,7 +123,7 @@ class TaskGateway ["dn"] ); // remove the count key from the arrays, keeping only DN. - unset($subTasksCompleted['count']); + $this->unsetCountKeys($subTasksCompleted); if (!empty($subTasksCompleted)) { foreach ($subTasksCompleted as $subTasks) { $result[$subTasks['dn']]['result'] = $this->removeSubTask($subTasks['dn']); @@ -298,16 +147,17 @@ class TaskGateway ["dn", "fdTasksRepeatableSchedule", "fdTasksLastExec", "fdTasksScheduleDate"] ); // remove the count key from the arrays, keeping only DN. - unset($tasks['count']); + $this->unsetCountKeys($tasks); if (!empty($tasks)) { + // Initiate the object webservice. - $webservice = new WebServiceCall($_ENV['FUSION_DIRECTORY_API_URL'] . '/login', 'POST'); + $webservice = new FusionDirectory\Rest\WebServiceCall($_ENV['FUSION_DIRECTORY_API_URL'] . '/login', 'POST'); // Required to prepare future webservice call. E.g. Retrieval of mandatory token. $webservice->setCurlSettings(); - // Is used to verify cyclic schedule with date format. - $now = new DateTime('now', new DateTimeZone('UTC')); + // Is used to verify cyclic schedule with date format. This use de local timezone - not UTC + $now = new DateTime('now'); foreach ($tasks as $task) { // Transform schedule time (it is a simple string) @@ -322,6 +172,7 @@ class TaskGateway // Case where the tasks were once run, verification of the cyclic schedule and last exec. } else if (!empty($task['fdtasksrepeatableschedule'][0])) { $lastExec = new DateTime($task['fdtaskslastexec'][0]); + // Efficient way to verify timelapse $interval = $now->diff($lastExec); @@ -341,21 +192,23 @@ class TaskGateway } break; case 'Weekly' : - if ($interval->d >= 7) { + if ($interval->days >= 7) { $result[$task['dn']]['result'] = $webservice->activateCyclicTasks($task['dn']); } else { $result[$task['dn']]['lastExecFailed'] = 'This cyclic task has yet to reached its next execution cycle.'; } break; case 'Daily' : - if ($interval->d >= 1) { + if ($interval->days >= 1) { $result[$task['dn']]['result'] = $webservice->activateCyclicTasks($task['dn']); } else { $result[$task['dn']]['lastExecFailed'] = 'This cyclic task has yet to reached its next execution cycle.'; } break; case 'Hourly' : - if ($interval->h >= 7) { + // When checking for hourly schedules, consider both the days and hours + $totalHours = $interval->days * 24 + $interval->h; + if ($totalHours >= 1) { $result[$task['dn']]['result'] = $webservice->activateCyclicTasks($task['dn']); } else { $result[$task['dn']]['lastExecFailed'] = 'This cyclic task has yet to reached its next execution cycle.'; @@ -365,7 +218,7 @@ class TaskGateway } // Case where cyclic tasks where found but the schedule is no ready. } else { - $result[$task['dn']]['Status'] = 'This cyclic task has yet to reach its scheduled date.'; + $result[$task['dn']]['Status'] = 'This cyclic task has yet to reach its next execution cycle.'; } } } else { @@ -375,88 +228,20 @@ class TaskGateway return $result; } - /** - * @param array $lifeCycleBehavior - * @param array $currentUserLifeCycle - * @return bool - * Note receive the life cycle behavior desired and compare it the received current user life cycle, returning TRUE - * if there is indeed a difference and therefore must update the user information. - * In case the comparison is impossible due to the use not having a status listed, it will report false. - */ - protected function isLifeCycleRequiringModification (array $lifeCycleBehavior, array $currentUserLifeCycle): bool - { - $result = FALSE; - // Regular expression in order to extract the supann format within an array - $pattern = '/\{(\w+)\}(\w):([^:]*)(?::([^:]*))?(?::([^:]*))?(?::([^:]*))?/'; - - // In case the tasks is launched without supann being activated on the user account, return error - if (empty($currentUserLifeCycle[0]['supannressourceetatdate'][0])) { - return FALSE; - } - // Perform the regular expression match - preg_match($pattern, $currentUserLifeCycle[0]['supannressourceetatdate'][0], $matches); - - // Extracting values of current user - $userSupann['Resource'] = $matches[1] ?? ''; - $userSupann['State'] = $matches[2] ?? ''; - $userSupann['SubState'] = $matches[3] ?? ''; - // Array index 4 is skipped, we only use end date to apply our life cycle logic. Start date has no use here. - $userSupann['EndDate'] = $matches[5] ?? ''; - - // Extracting values of desired pre-state behavior - $preStateSupann['Resource'] = $lifeCycleBehavior[0]['fdtaskslifecyclepreresource'][0]; - $preStateSupann['State'] = $lifeCycleBehavior[0]['fdtaskslifecycleprestate'][0]; - $preStateSupann['SubState'] = $lifeCycleBehavior[0]['fdtaskslifecyclepresubstate'][0] ?? ''; //SubState is optional - - // Verifying if the user end date for selected resource is overdue - if (!empty($userSupann['EndDate']) && strtotime($userSupann['EndDate']) <= time()) { - // Comparing value in a nesting conditions - if ($userSupann['Resource'] == $preStateSupann['Resource']) { - if ($userSupann['State'] == $preStateSupann['State']) { - // as SubState is optional, if both resource and state match at this point, modification is allowed. - if (empty($preStateSupann['SubState'])) { - $result = TRUE; - } else if ($preStateSupann['SubState'] == $userSupann['SubState']) { - $result = TRUE; - } - } - } - } - - return $result; - } - - /** - * @param array $fdTasksConf - * @return bool - * Note : Method which verify the last executed e-mails sent - * Verify if the time interval is respected in order to protect from SPAM - */ - public function verifySpamProtection (array $fdTasksConf): bool - { - $lastExec = $fdTasksConf[0]["fdtasksconflastexectime"][0] ?? NULL; - $spamInterval = $fdTasksConf[0]["fdtasksconfintervalemails"][0] ?? NULL; - - // Multiplication is required to have the seconds - $spamInterval = $spamInterval * 60; - $antispam = $lastExec + $spamInterval; - if ($antispam <= time()) { - return TRUE; - } - - return FALSE; - } - /** * @param string $schedule * @return bool + * @throws Exception + * Note : Verification of the schedule in complete string format and compare. + * DateTime will use the system timezone by default. */ - // Verification of the schedule in complete string format and compare. public function verifySchedule (string $schedule): bool { - $schedule = strtotime($schedule); - if ($schedule < time()) { - return TRUE; + $currentDateTime = new DateTime('now'); // Get current datetime in locale timezone + $scheduledDateTime = new DateTime($schedule); // Parse scheduled datetime string in local timezone + + if ($scheduledDateTime < $currentDateTime) { + return TRUE; // Schedule has passed } return FALSE; @@ -513,7 +298,10 @@ class TaskGateway public function updateTaskStatus (string $dn, string $cn, string $status) { // prepare data - $ldap_entry["cn"] = $cn; + if (!empty($dn)) { + $ldap_entry["cn"] = $cn; + } + // Status subject to change $ldap_entry["fdTasksGranularStatus"] = $status; @@ -527,6 +315,21 @@ class TaskGateway return $result; } + /** + * @param $objectType + * @return array|string[]|void + */ + public function getObjectTypeTask ($objectType) + { + $task = $this->getTask($objectType); + if (!$task) { + TaskController::respondNotFound($objectType); + exit; + } + + return $task; + } + /** * @param string $dn * @return bool|string @@ -534,7 +337,6 @@ class TaskGateway */ public function updateLastMailExecTime (string $dn) { - // prepare data $ldap_entry["fdTasksConfLastExecTime"] = time(); // Add data to LDAP @@ -546,5 +348,4 @@ class TaskGateway } return $result; } - } \ No newline at end of file diff --git a/library/interfaces/EndpointInterface.php b/library/interfaces/EndpointInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c9f8f30f6eb2473cb699378d0ed2b2cf7196b15c --- /dev/null +++ b/library/interfaces/EndpointInterface.php @@ -0,0 +1,34 @@ +<?php + +interface EndpointInterface +{ + + public function __construct (TaskGateway $gateway); + + /** + * @return array + * Part of the interface of orchestrator plugin to treat GET method + */ + public function processEndPointGet (): array; + + /** + * @param array|NULL $data + * @return array + * Note : Part of the interface of orchestrator plugin to treat POST method + */ + public function processEndPointPost (array $data = NULL): array; + + /** + * @param array|NULL $data + * @return array + * Note : Part of the interface of orchestrator plugin to treat PATCH method + */ + public function processEndPointPatch (array $data = NULL): array; + + /** + * @param array|NULL $data + * @return array + * Note : Part of the interface of orchestrator plugin to treat DELETE method + */ + public function processEndPointDelete (array $data = NULL): array; +} diff --git a/plugins/tasks/Audit.php b/plugins/tasks/Audit.php new file mode 100644 index 0000000000000000000000000000000000000000..6e51ae7e26d1dcc5413f6ee01972d7f317bc0893 --- /dev/null +++ b/plugins/tasks/Audit.php @@ -0,0 +1,145 @@ +<?php + +class Audit implements EndpointInterface +{ + + private TaskGateway $gateway; + + public function __construct (TaskGateway $gateway) + { + $this->gateway = $gateway; + } + + /** + * @return array + * Part of the interface of orchestrator plugin to treat GET method + */ + public function processEndPointGet (): array + { + return []; + } + + /** + * @param array|null $data + * @return array + */ + public function processEndPointPost (array $data = NULL): array + { + return []; + } + + /** + * @param array|NULL $data + * @return array + */ + public function processEndPointDelete (array $data = NULL): array + { + return []; + } + + /** + * @param array|NULL $data + * @return array + * @throws Exception + */ + public function processEndPointPatch (array $data = NULL): array + { + $result = $this->processAuditDeletion($this->gateway->getObjectTypeTask('Audit')); + + // Recursive function to filter out empty arrays at any depth + $nonEmptyResults = $this->recursiveArrayFilter($result); + + if (!empty($nonEmptyResults)) { + return $nonEmptyResults; + } else { + return ['No audit requiring removal']; + } + } + + /** + * @param array $auditSubTasks + * @return array + * @throws Exception + */ + public function processAuditDeletion (array $auditSubTasks): array + { + $result = []; + + foreach ($auditSubTasks as $task) { + + // If the tasks must be treated - status and scheduled - process the sub-tasks + if ($this->gateway->statusAndScheduleCheck($task)) { + + // Retrieve data from the main task. + $auditMainTask = $this->getAuditMainTask($task['fdtasksgranularmaster'][0]); + // Simply get the days to retain audit. + $auditRetention = $auditMainTask[0]['fdaudittasksretention'][0]; + + // Verification of all audit and their potential removal based on retention days passed, also update subtasks. + $result[] = $this->checkAuditPassedRetention($auditRetention, $task['dn'], $task['cn'][0]); + } + } + + return $result; + } + + /** + * @param string $mainTaskDn + * @return array + * Note : Simply return attributes from the main related audit tasks. + */ + public function getAuditMainTask (string $mainTaskDn): array + { + // Retrieve data from the main task + return $this->gateway->getLdapTasks('(objectClass=fdAuditTasks)', ['fdAuditTasksRetention'], '', $mainTaskDn); + } + + /** + * @param $auditRetention + * @return array + * Note : This will return a validation of audit log suppression + * @throws Exception + */ + public function checkAuditPassedRetention ($auditRetention, $subTaskDN, $subTaskCN): array + { + $auditLib = new FusionDirectory\Audit\AuditLib($auditRetention, $this->returnLdapAuditEntries(), $this->gateway, $subTaskDN, $subTaskCN); + return $auditLib->checkAuditPassedRetentionOrchestrator(); + } + + /** + * @return array + * NOTE : simply return the list of audit entries existing in LDAP + */ + public function returnLdapAuditEntries () : array + { + // Search in LDAP for audit entries (All entries ! This can be pretty heavy. + $audit = $this->gateway->getLdapTasks('(objectClass=fdAuditEvent)', ['fdAuditDateTime'], '', ''); + // Remove the count key from the audit array. + $this->gateway->unsetCountKeys($audit); + + return $audit; + } + + /** + * @param array $array + * @return array + * Note : Recursively filters out empty values and arrays at any depth. + */ + private function recursiveArrayFilter (array $array): array + { + // First filter the array for non-empty elements + $filtered = array_filter($array, function ($item) { + if (is_array($item)) { + // Recursively filter the sub-array + $item = $this->recursiveArrayFilter($item); + // Only retain non-empty arrays + return !empty($item); + } else { + // Retain non-empty scalar values + return !empty($item); + } + }); + + return $filtered; + } +} \ No newline at end of file diff --git a/plugins/tasks/LifeCycle.php b/plugins/tasks/LifeCycle.php new file mode 100644 index 0000000000000000000000000000000000000000..02bec2a423e6123943d79fc12389a607fd0c1d5e --- /dev/null +++ b/plugins/tasks/LifeCycle.php @@ -0,0 +1,313 @@ +<?php + + +class LifeCycle implements EndpointInterface +{ + private TaskGateway $gateway; + + function __construct (TaskGateway $gateway) + { + $this->gateway = $gateway; + } + + /** + * @return array + * Part of the interface of orchestrator plugin to treat GET method + */ + public function processEndPointGet (): array + { + return []; + } + + /** + * @param array|null $data + * @return array + * Note : Part of the interface of orchestrator plugin to treat POST method + */ + public function processEndPointPost (array $data = NULL): array + { + return []; + } + + /** + * @param array|null $data + * @return array + * Note : Part of the interface of orchestrator plugin to treat DELETE method + */ + public function processEndPointDelete (array $data = NULL): array + { + return []; + } + + /** + * @param array|null $data + * @return array + * @throws Exception + * Note : Part of the interface of orchestrator plugin to treat PATCH method + */ + public function processEndPointPatch (array $data = NULL): array + { + return $this->processLifeCycleTasks($this->gateway->getObjectTypeTask('lifeCycle')); + } + + /** + * @param array $list_tasks + * @return array[]|string[] + * @throws Exception + * Note : Verify the status and schedule as well as searching for the correct life cycle behavior from main task. + */ + public function processLifeCycleTasks (array $list_tasks): array + { + // Array representing the status of the subtask. + $result = []; + // Initiate the object webservice. + $webservice = new FusionDirectory\Rest\WebServiceCall($_ENV['FUSION_DIRECTORY_API_URL'] . '/login', 'POST'); + // Required to prepare future webservice call. E.g. Retrieval of mandatory token. + $webservice->setCurlSettings(); + + foreach ($list_tasks as $task) { + // If the tasks must be treated - status and scheduled - process the sub-tasks + if ($this->gateway->statusAndScheduleCheck($task)) { + + // Simply retrieve the lifeCycle behavior from the main related tasks, sending the dns and desired attributes + $lifeCycleBehavior = $this->getLifeCycleBehaviorFromMainTask($task['fdtasksgranularmaster'][0]); + + // Simply retrieve the current supannStatus of the user DN related to the task at hand. + $currentUserLifeCycle = $this->getUserSupannHistory($task['fdtasksgranulardn'][0]); + + // Compare both the required schedule and the current user status - returning TRUE if modification is required. + if ($this->isLifeCycleRequiringModification($lifeCycleBehavior, $currentUserLifeCycle)) { + + // This will call a method to modify the ressourcesSupannEtatDate of the DN linked to the subTask + $lifeCycleResult = $this->updateLifeCycle($lifeCycleBehavior, $task['fdtasksgranulardn'][0], $currentUserLifeCycle); + + if ($lifeCycleResult === TRUE) { + + $result[$task['dn']]['results'] = json_encode("Account states have been successfully modified for " . $task['fdtasksgranulardn'][0]); + // Status of the task must be updated to success + $updateResult = $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], '2'); + + // Here the user is refresh in order to activate methods based on supann Status changes. + $result[$task['dn']]['refreshUser'] = $webservice->refreshUserInfo($task['fdtasksgranulardn'][0]); + + // In case the modification failed + } else { + $result[$task['dn']]['results'] = json_encode("Error updating " . $task['fdtasksgranulardn'][0] . "-" . $lifeCycleResult); + // Update of the task status error message + $updateResult = $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], $lifeCycleResult); + } + // Verification if the sub-task status has been updated correctly + if ($updateResult === TRUE) { + $result[$task['dn']]['statusUpdate'] = 'Success'; + } else { + $result[$task['dn']]['statusUpdate'] = $updateResult; + } + // Remove the subtask has it is not required to update it nor to process it. + } else { + $result[$task['dn']]['results'] = 'Sub-task removed for : ' . $task['fdtasksgranulardn'][0] . ' with result : ' + . $this->gateway->removeSubTask($task['dn']); + $result[$task['dn']]['statusUpdate'] = 'No updates required, sub-task will be removed.'; + } + } + } + // If array is empty, no tasks of type life cycle needs to be treated. + if (empty($result)) { + $result = 'No tasks of type "Life Cycle" requires processing.'; + } + return [$result]; + } + + /** + * @param array $lifeCycleBehavior + * @param array $currentUserLifeCycle + * @return bool + * Note receive the life cycle behavior desired and compare it the received current user life cycle, returning TRUE + * if there is indeed a difference and therefore must update the user information. + * In case the comparison is impossible due to the use not having a status listed, it will report false. + */ + protected function isLifeCycleRequiringModification (array $lifeCycleBehavior, array $currentUserLifeCycle): bool + { + $result = FALSE; + // Regular expression in order to extract the supann format within an array + $pattern = '/\{(\w+)\}(\w):([^:]*)(?::([^:]*))?(?::([^:]*))?(?::([^:]*))?/'; + + // In case the tasks is launched without supann being activated on the user account, return error + if (empty($currentUserLifeCycle[0]['supannressourceetatdate'][0])) { + return FALSE; + } + + // Extracting values of desired pre-state behavior + $preStateSupann['Resource'] = $lifeCycleBehavior[0]['fdtaskslifecyclepreresource'][0]; + $preStateSupann['State'] = $lifeCycleBehavior[0]['fdtaskslifecycleprestate'][0]; + $preStateSupann['SubState'] = $lifeCycleBehavior[0]['fdtaskslifecyclepresubstate'][0] ?? ''; //SubState is optional + + // Iteration of all potential existing supann states of the user in order to find a match + foreach ($currentUserLifeCycle[0]['supannressourceetatdate'] as $resource) { + // Perform the regular expression match + preg_match($pattern, $resource, $matches); + + // Extracting values of current user + $userSupann['Resource'] = $matches[1] ?? ''; + $userSupann['State'] = $matches[2] ?? ''; + $userSupann['SubState'] = $matches[3] ?? ''; + // Array index 4 is skipped, we only use end date to apply our life cycle logic. Start date has no use here. + $userSupann['EndDate'] = $matches[5] ?? ''; + + // Verifying if the user end date for selected resource is overdue + if (!empty($userSupann['EndDate']) && strtotime($userSupann['EndDate']) <= time()) { + // Comparing value in a nesting conditions + if ($userSupann['Resource'] == $preStateSupann['Resource']) { + if ($userSupann['State'] == $preStateSupann['State']) { + // as SubState is optional, if both resource and state match at this point, modification is allowed. + if (empty($preStateSupann['SubState'])) { + $result = TRUE; + } else if ($preStateSupann['SubState'] == $userSupann['SubState']) { + $result = TRUE; + } + } + } + } + } + + return $result; + } + + /** + * @param array $lifeCycleBehavior + * @param string $userDN + * @param array $currentUserLifeCycle + * @return bool|string + * Note receive the required behavior and the previous list of supann state to update in LDAP. + */ + protected function updateLifeCycle (array $lifeCycleBehavior, string $userDN, array $currentUserLifeCycle) + { + // Init return value + $result = ''; + // Hosting the final entry of supann attributes to be pushed to LDAP + $ldapEntry = []; + + // Only keep the supann state from the received array and removing the count key + $userStateHistory = $currentUserLifeCycle[0]['supannressourceetatdate']; + $this->gateway->unsetCountKeys($userStateHistory); + + // Extracting values of desired post-state behavior + $newEntry = $this->prepareNewEntry($lifeCycleBehavior[0]); + + // Create the new resource without start / end date + $newResource = "{" . $newEntry['Resource'] . "}" . $newEntry['State'] . ":" . $newEntry['SubState']; + // Get the resource name, it will be used to compare if the resource exists in history + $newResourceName = $this->returnSupannResourceBetweenBrackets($newResource); + + // Find a matching resource in the user state history + $matchedResource = $this->findMatchedResource($userStateHistory, $newResourceName); + if ($matchedResource) { + + // Fetch the end date of the matched resource. + $currentEndDate = $this->extractCurrentEndDate($matchedResource); + // Create a DateTime object from the string + $currentEndDateObject = DateTime::createFromFormat("Ymd", $currentEndDate); + $currentEndDateObject->modify("+" . $newEntry['EndDate'] . " days"); + $finalRessourceEtatDate = $newResource . ':' . $currentEndDate . ':' . $currentEndDateObject->format('Ymd'); + + // Iterate again through the supann state and get a match + foreach ($userStateHistory as $userState => $value) { + // Extract resource in curly braces (brackets) from the current supannRessourceEtatDate + $currentResource = $this->returnSupannResourceBetweenBrackets($value); + + // Get the resource matched + if ($currentResource === $newResourceName) { + $userStateHistory[$userState] = $finalRessourceEtatDate; + break; + } + } + + // Creation of the ldap entry + $ldapEntry['supannRessourceEtatDate'] = $userStateHistory; + try { + $result = ldap_modify($this->gateway->ds, $userDN, $ldapEntry); + } catch (Exception $e) { + $result = json_encode(["Ldap Error" => "$e"]); + } + } + return $result; + } + + /** + * @param array $userStateHistory + * @param string $newResourceName + * @return string|null + * Note : Simple helper method to return the matched resource. + */ + private function findMatchedResource (array $userStateHistory, string $newResourceName): ?string + { + foreach ($userStateHistory as $value) { + if ($this->returnSupannResourceBetweenBrackets($value) === $newResourceName) { + return $value; + } + } + return NULL; + } + + /** + * @param array $lifeCycleBehavior + * @return array + * Simple helper method for readiness. + */ + private function prepareNewEntry (array $lifeCycleBehavior): array + { + return [ + 'Resource' => $lifeCycleBehavior['fdtaskslifecyclepostresource'][0], + 'State' => $lifeCycleBehavior['fdtaskslifecyclepoststate'][0], + 'SubState' => $lifeCycleBehavior['fdtaskslifecyclepostsubstate'][0] ?? '', + 'EndDate' => $lifeCycleBehavior['fdtaskslifecyclepostenddate'][0] ?? 0, + ]; + } + + /** + * @param string|null $matchedResource + * @return string + * Note : Simply return the end date of a supann ressource etat date + */ + private function extractCurrentEndDate (?string $matchedResource): string + { + $parts = explode(":", $matchedResource); + // Get the last element, which is the date + return end($parts); + } + + /** + * @param string $supannRessourceEtatDate + * @return string|null + * Note : Simple method to return the content between {} of a supannRessourceEtatDate. + */ + private function returnSupannResourceBetweenBrackets (string $supannRessourceEtatDate): ?string + { + preg_match('/\{(.*?)\}/', $supannRessourceEtatDate, $matches); + return $matches[1] ?? NULL; + } + + /** + * @param string $taskDN + * @return array + * Note : Simply return attributes from main task, here supann desired behavior + */ + private function getLifeCycleBehaviorFromMainTask (string $taskDN): array + { + return $this->gateway->getLdapTasks('(objectClass=*)', ['fdTasksLifeCyclePreResource', + 'fdTasksLifeCyclePreState', 'fdTasksLifeCyclePreSubState', + 'fdTasksLifeCyclePostResource', 'fdTasksLifeCyclePostState', 'fdTasksLifeCyclePostSubState', 'fdTasksLifeCyclePostEndDate'], + '', $taskDN); + } + + /** + * @param $userDN + * @return array + * Note : simply return the current values of supannRessourceEtatDate of the specified user. + */ + private function getUserSupannHistory ($userDN): array + { + return $this->gateway->getLdapTasks('(objectClass=supannPerson)', ['supannRessourceEtatDate'], + '', $userDN); + } + +} \ No newline at end of file diff --git a/plugins/tasks/Mail.php b/plugins/tasks/Mail.php new file mode 100644 index 0000000000000000000000000000000000000000..55ef06e9ebe11cc0c6cfff9dc5d6874989d4d9b3 --- /dev/null +++ b/plugins/tasks/Mail.php @@ -0,0 +1,192 @@ +<?php + + +class Mail implements EndpointInterface +{ + private TaskGateway $gateway; + + function __construct (TaskGateway $gateway) + { + $this->gateway = $gateway; + } + + /** + * @return array + * Part of the interface of orchestrator plugin to treat GET method + */ + public function processEndPointGet (): array + { + return []; + } + + /** + * @return array + * Note : Part of the interface of orchestrator plugin to treat POST method + */ + public function processEndPointPost (array $data = NULL): array + { + return []; + } + + /** + * @return array + * Note : Part of the interface of orchestrator plugin to treat DELETE method + */ + public function processEndPointDelete (array $data = NULL): array + { + return []; + } + + /** + * @param array|NULL $data + * @return array + * @throws Exception + * Note : Part of the interface of orchestrator plugin to treat PATCH method + */ + public function processEndPointPatch (array $data = NULL): array + { + return $this->processMailTasks($this->gateway->getObjectTypeTask('Mail Object')); + } + + /** + * @param array $tasks + * @return array + * @throws Exception + */ + public function processMailTasks (array $tasks): array + { + $result = []; + $fdTasksConf = $this->getMailObjectConfiguration(); + $maxMailsConfig = $this->returnMaximumMailToBeSend($fdTasksConf); + + // Increment for anti=spam, starts at 0, each mail task only contain one email, addition if simply + one. + $maxMailsIncrement = 0; + + if ($this->verifySpamProtection($fdTasksConf)) { + // Note : if list_tasks is empty, the controller receive null as result and will log/process it properly. + foreach ($tasks as $mail) { + + + // verify status before processing (to be checked with schedule as well). + if ($mail["fdtasksgranularstatus"][0] == 1 && $this->gateway->verifySchedule($mail["fdtasksgranularschedule"][0])) { + + // Search for the related attached mail object. + $mailInfos = $this->retrieveMailTemplateInfos($mail["fdtasksgranularref"][0]); + $mailContent = $mailInfos[0]; + + // Only takes arrays related to files attachments for the mail template selected + unset($mailInfos[0]); + // Remove count from array. + $this->gateway->unsetCountKeys($mailInfos); + $mailAttachments = array_values($mailInfos); + + $setFrom = $mail["fdtasksgranularmailfrom"][0]; + $setBCC = $mail["fdtasksgranularmailbcc"][0] ?? NULL; + $recipients = $mail["fdtasksgranularmail"]; + $body = $mailContent["fdmailtemplatebody"][0]; + $signature = $mailContent["fdmailtemplatesignature"][0] ?? NULL; + $subject = $mailContent["fdmailtemplatesubject"][0]; + $receipt = $mailContent["fdmailtemplatereadreceipt"][0]; + + foreach ($mailAttachments as $file) { + $fileInfo['cn'] = $file['cn'][0]; + $fileInfo['content'] = $file['fdmailattachmentscontent'][0]; + $attachments[] = $fileInfo; + } + + // Required before passing the array to the constructor mail. + if (empty($attachments)) { + $attachments = NULL; + } + + $mail_controller = new \FusionDirectory\Mail\MailLib($setFrom, + $setBCC, + $recipients, + $body, + $signature, + $subject, + $receipt, + $attachments); + + $mailSentResult = $mail_controller->sendMail(); + + if ($mailSentResult[0] == "SUCCESS") { + + // The third arguments "2" is the status code of success for mail as of now 18/11/22 + $result[$mail["dn"]]['statusUpdate'] = $this->gateway->updateTaskStatus($mail["dn"], $mail["cn"][0], "2"); + $result[$mail["dn"]]['mailStatus'] = 'mail : ' . $mail["dn"] . ' was successfully sent'; + $result[$mail["dn"]]['updateLastMailExec'] = $this->gateway->updateLastMailExecTime($fdTasksConf[0]["dn"]); + + } else { + $result[$mail["dn"]]['statusUpdate'] = $this->gateway->updateTaskStatus($mail["dn"], $mail["cn"][0], $mailSentResult[0]); + $result[$mail["dn"]]['Error'] = $mailSentResult; + } + + // Verification anti-spam max mails to be sent and quit loop if matched + $maxMailsIncrement += 1; //Only one as recipients in mail object is always one email. + if ($maxMailsIncrement == $maxMailsConfig) { + break; + } + + } + } + } + + return $result; + } + + /** + * @return array + * Note : A simple retrieval methods of the mail backend configuration set in FusionDirectory + */ + private function getMailObjectConfiguration (): array + { + return $this->gateway->getLdapTasks( + "(objectClass=fdTasksConf)", + ["fdTasksConfLastExecTime", "fdTasksConfIntervalEmails", "fdTasksConfMaxEmails"] + ); + } + + /** + * @param array $fdTasksConf + * @return int + * Note : Allows a safety check in case mail configuration backed within FD has been missed. (50). + */ + public function returnMaximumMailToBeSend (array $fdTasksConf): int + { + // set the maximum mails to be sent to the configured value or 50 if not set. + return $fdTasksConf[0]["fdtasksconfmaxemails"][0] ?? 50; + } + + /** + * @param array $fdTasksConf + * @return bool + * Note : Method which verify the last executed e-mails sent + * Verify if the time interval is respected in order to protect from SPAM + */ + public function verifySpamProtection (array $fdTasksConf): bool + { + $lastExec = $fdTasksConf[0]["fdtasksconflastexectime"][0] ?? NULL; + $spamInterval = $fdTasksConf[0]["fdtasksconfintervalemails"][0] ?? NULL; + + // Multiplication is required to have the seconds + $spamInterval = $spamInterval * 60; + $antispam = $lastExec + $spamInterval; + if ($antispam <= time()) { + return TRUE; + } + + return FALSE; + } + + /** + * @param string $templateName + * @return array + * Note :simply retrieve all information linked to a mail template object. + */ + public function retrieveMailTemplateInfos (string $templateName): array + { + return $this->gateway->getLdapTasks("(|(objectClass=fdMailTemplate)(objectClass=fdMailAttachments))", [], $templateName); + } + +} diff --git a/plugins/tasks/Notifications.php b/plugins/tasks/Notifications.php new file mode 100644 index 0000000000000000000000000000000000000000..f78ef5ecac235d5b426a56db527e881821d2f1c1 --- /dev/null +++ b/plugins/tasks/Notifications.php @@ -0,0 +1,431 @@ +<?php + +class Notifications implements EndpointInterface +{ + + private TaskGateway $gateway; + + public function __construct (TaskGateway $gateway) + { + $this->gateway = $gateway; + } + + /** + * @return array + * Part of the interface of orchestrator plugin to treat GET method + */ + public function processEndPointGet (): array + { + return []; + } + + /** + * @param array|null $data + * @return array + */ + public function processEndPointPost (array $data = NULL): array + { + return []; + } + + /** + * @param array|NULL $data + * @return array + * @throws Exception + */ + public function processEndPointPatch (array $data = NULL): array + { + return $this->processNotifications($this->gateway->getObjectTypeTask('notifications')); + } + + /** + * @param array|NULL $data + * @return array + */ + public function processEndPointDelete (array $data = NULL): array + { + return []; + } + + /** + * @param array $notificationsSubTasks + * @return array + * @throws Exception + */ + public function processNotifications (array $notificationsSubTasks): array + { + $result = []; + // It will contain all required notifications to be sent per main task. + $notifications = []; + + foreach ($notificationsSubTasks as $task) { + // If the tasks must be treated - status and scheduled - process the sub-tasks + if ($this->gateway->statusAndScheduleCheck($task)) { + + // Retrieve data from the main task + $notificationsMainTask = $this->getNotificationsMainTask($task['fdtasksgranularmaster'][0]); + $notificationsMainTaskName = $task['fdtasksgranularmaster'][0]; + + // Generate the mail form with all mail controller requirements + $mailTemplateForm = $this->generateMainTaskMailTemplate($notificationsMainTask); + + // Simply retrieve the list of audited attributes + $auditAttributes = $this->decodeAuditAttributes($task); + + // Recovering monitored attributes list from the defined notification task. + $monitoredAttrs = $notificationsMainTask[0]['fdtasksnotificationsattributes']; + // Reformat supann + $monitoredSupannResource = $this->getSupannResourceState($notificationsMainTask[0]); + + // Simply remove keys with 'count' reported by ldap. + $this->gateway->unsetCountKeys($monitoredAttrs); + $this->gateway->unsetCountKeys($monitoredSupannResource); + + // Find matching attributes between audited and monitored attributes + $matchingAttrs = $this->findMatchingAttributes($auditAttributes, $monitoredAttrs); + + // Verify Supann resource state if applicable + if ($this->shouldVerifySupannResource($monitoredSupannResource, $auditAttributes)) { + // Adds it to the mating attrs for further notification process. + $matchingAttrs[] = 'supannRessourceEtat'; + } + + if (!empty($matchingAttrs)) { + // Fill an array with UID of audited user and related matching attributes + $notifications[$notificationsMainTaskName]['subTask'][$task['cn'][0]]['attrs'] = $matchingAttrs; + + // Require to be set for updating the status of the task later on. + $notifications[$notificationsMainTaskName]['subTask'][$task['cn'][0]]['dn'] = $task['dn']; + $notifications[$notificationsMainTaskName]['subTask'][$task['cn'][0]]['uid'] = $task['fdtasksgranulardn'][0]; + $notifications[$notificationsMainTaskName]['mailForm'] = $mailTemplateForm; + // Overwrite array notifications with complementing mail form body with uid and related attributes. + $notifications = $this->completeNotificationsBody($notifications, $notificationsMainTaskName); + + } else { // Simply remove the subTask has no notifications are required + $result[$task['dn']]['Removed'] = $this->gateway->removeSubTask($task['dn']); + $result[$task['dn']]['Status'] = 'No matching audited attributes with monitored attributes, safely removed!'; + } + } + } + + if (!empty($notifications)) { + $result[] = $this->sendNotificationsMail($notifications); + } + + return $result; + } + + /** + * Determine if Supann resource verification is needed. + * + * @param array $monitoredSupannResource + * @param array|null $auditAttributes + * @return bool + */ + private function shouldVerifySupannResource (array $monitoredSupannResource, ?array $auditAttributes): bool + { + if (!empty($auditAttributes)) { + return $monitoredSupannResource['resource'][0] !== 'NONE' && + $this->verifySupannState($monitoredSupannResource, $auditAttributes); + } + return FALSE; + } + + /** + * Get the Supann resource state. + * + * @param array $notificationsMainTask + * @return array + */ + private function getSupannResourceState (array $notificationsMainTask): array + { + return [ + 'resource' => $notificationsMainTask['fdtasksnotificationsresource'], + 'state' => $notificationsMainTask['fdtasksnotificationsstate'], + 'subState' => $notificationsMainTask['fdtasksnotificationssubstate'] ?? NULL + ]; + } + + /** + * Decode audit attributes from the task. + * + * @param array $task + * @return array + */ + private function decodeAuditAttributes (array $task): array + { + $auditAttributesJson = $this->retrieveAuditedAttributes($task); + $auditAttributes = []; + + // Decoding the json_format into an associative array, implode allows to put all values of array together.(forming the json correctly). + foreach ($auditAttributesJson as $auditAttribute) { + $auditAttributes[] = json_decode(implode($auditAttribute), TRUE); + } + + return $auditAttributes; + } + + /** + * Find matching attributes between audit and monitored attributes. + * + * @param array|null $auditAttributes + * @param array $monitoredAttrs + * @return array + */ + private function findMatchingAttributes (?array $auditAttributes, array $monitoredAttrs): array + { + $matchingAttrs = []; + + if (!empty($auditAttributes)) { + foreach ($auditAttributes as $attributeName) { + foreach ($monitoredAttrs as $monitoredAttr) { + if (!empty($attributeName) && array_key_exists($monitoredAttr, $attributeName)) { + $matchingAttrs[] = $monitoredAttr; + } + } + } + } + + return $matchingAttrs; + } + + /** + * @param array $supannResource + * @param array $auditedAttrs + * @return bool + * Note : Create the supann format and check for a match. + */ + private function verifySupannState (array $supannResource, array $auditedAttrs): bool + { + $result = FALSE; + + //Construct Supann Resource State as string + if (!empty($supannResource['subState'][0])) { + $monitoredSupannState = '{' . $supannResource['resource'][0] . '}' . $supannResource['state'][0] . ':' . $supannResource['subState'][0]; + } else { + $monitoredSupannState = '{' . $supannResource['resource'][0] . '}' . $supannResource['state'][0]; + } + + // Get all the values only of a multidimensional array. + $auditedValues = $this->getArrayValuesRecursive($auditedAttrs); + + if (in_array($monitoredSupannState, $auditedValues)) { + $result = TRUE; + } else { + $result = FALSE; + } + + return $result; + } + + /** + * @param $array + * @return array + * Note : simply return all values of a multi-dimensional array. + */ + public function getArrayValuesRecursive ($array) + { + $values = []; + foreach ($array as $value) { + if (is_array($value)) { + // If value is an array, merge its values recursively + $values = array_merge($values, $this->getArrayValuesRecursive($value)); + } else { + // If value is not an array, add it to the result + $values[] = $value; + } + } + return $values; + } + + /** + * @param string $mainTaskDn + * @return array + */ + public function getNotificationsMainTask (string $mainTaskDn): array + { + // Retrieve data from the main task + return $this->gateway->getLdapTasks('(objectClass=fdTasksNotifications)', ['fdTasksNotificationsListOfRecipientsMails', + 'fdTasksNotificationsAttributes', 'fdTasksNotificationsMailTemplate', 'fdTasksNotificationsEmailSender', + 'fdTasksNotificationsSubState', 'fdTasksNotificationsState', 'fdTasksNotificationsResource'], + '', $mainTaskDn); + } + + /** + * @param array $mainTask + * @return array + * Note : Simply generate the email to be sent as notification. + */ + private function generateMainTaskMailTemplate (array $mainTask): array + { + // Generate email configuration for each result of subtasks having the same main task.w + $recipients = $mainTask[0]["fdtasksnotificationslistofrecipientsmails"]; + $this->gateway->unsetCountKeys($recipients); + $sender = $mainTask[0]["fdtasksnotificationsemailsender"][0]; + $mailTemplateName = $mainTask[0]['fdtasksnotificationsmailtemplate'][0]; + + $mailInfos = $this->gateway->getLdapTasks("(|(objectClass=fdMailTemplate)(objectClass=fdMailAttachments))", [], $mailTemplateName); + $mailContent = $mailInfos[0]; + + // Set the notification array with all required variable for all sub-tasks of same main task origin. + $mailForm['setFrom'] = $sender; + $mailForm['recipients'] = $recipients; + $mailForm['body'] = $mailContent["fdmailtemplatebody"][0]; + $mailForm['signature'] = $mailContent["fdmailtemplatesignature"][0] ?? NULL; + $mailForm['subject'] = $mailContent["fdmailtemplatesubject"][0]; + $mailForm['receipt'] = $mailContent["fdmailtemplatereadreceipt"][0]; + + return $mailForm; + } + + /** + * @param array $notificationTask + * @return array + * NOTE : receive a unique tasks of type notification (one subtask at a time) + */ + protected function retrieveAuditedAttributes (array $notificationTask): array + { + $auditAttributes = []; + $auditInformation = []; + + // Retrieve audit data attributes from the list of references set in the sub-task + if (!empty($notificationTask['fdtasksgranularref'])) { + // Remove count keys (count is shared by ldap). + $this->gateway->unsetCountKeys($notificationTask); + + foreach ($notificationTask['fdtasksgranularref'] as $auditDN) { + $auditInformation[] = $this->gateway->getLdapTasks('(&(objectClass=fdAuditEvent))', + ['fdAuditAttributes'], '', $auditDN); + } + + // Again remove key: count retrieved from LDAP. + $this->gateway->unsetCountKeys($auditInformation); + // It is possible that an audit does not contain any attributes changes, condition is required. + foreach ($auditInformation as $attr) { + if (!empty($attr[0]['fdauditattributes'])) { + // Clear and compact received results from above ldap search + $auditAttributes[] = $attr[0]['fdauditattributes']; + } + } + } + + return $auditAttributes; + } + + /** + * @param array $notifications + * @param string $notificationsMainTaskName + * @return array + * Note : This method is present to add to the mailForm body the proper uid and attrs info. + */ + private function completeNotificationsBody (array $notifications, string $notificationsMainTaskName): array + { + // Iterate through each subTask and related attrs + $uidAttrsText = []; + + foreach ($notifications[$notificationsMainTaskName]['subTask'] as $value) { + $uidName = $value['uid']; + $attrs = []; + + foreach ($value['attrs'] as $attr) { + $attrs[] = $attr; + } + $uidAttrsText[] = "\n$uidName attrs=[" . implode(', ', $attrs) . "]"; + } + + // Make the array unique, avoiding uid and same attribute duplication. + $uidAttrsText = array_unique($uidAttrsText); + // Add uid names and related attrs to mailForm['body'] + $notifications[$notificationsMainTaskName]['mailForm']['body'] .= " " . implode(" ", $uidAttrsText); + + return $notifications; + } + + /** + * @param array $notifications + * @return array + * Note : Collect information and send notification email. + */ + protected function sendNotificationsMail (array $notifications): array + { + $result = []; + // Re-use of the same mail processing template logic + $fdTasksConf = $this->gateway->getLdapTasks( + "(objectClass=fdTasksConf)", + ["fdTasksConfLastExecTime", "fdTasksConfIntervalEmails", "fdTasksConfMaxEmails"] + ); + $maxMailsConfig = $fdTasksConf[0]["fdtasksconfmaxemails"][0] ?? 50; + + /* + Increment var starts a zero and added values will be the humber or recipients per main tasks, as one mail is + sent per main task. + */ + $maxMailsIncrement = 0; + + foreach ($notifications as $data) { + $numberOfRecipients = count($data['mailForm']['recipients']); + + $mail_controller = new \FusionDirectory\Mail\MailLib( + $data['mailForm']['setFrom'], + NULL, + $data['mailForm']['recipients'], + $data['mailForm']['body'], + $data['mailForm']['signature'], + $data['mailForm']['subject'], + $data['mailForm']['receipt'], + NULL + ); + + $mailSentResult = $mail_controller->sendMail(); + $result[] = $this->processMailResponseAndUpdateTasks($mailSentResult, $data, $fdTasksConf); + + // Verification anti-spam max mails to be sent and quit loop if matched. + $maxMailsIncrement += $numberOfRecipients; + if ($maxMailsIncrement == $maxMailsConfig) { + break; + } + } + + return $result; + } + + /** + * @param array $serverResults + * @param array $subTask + * @param array $mailTaskBackend + * @return array + * Note : + */ + protected function processMailResponseAndUpdateTasks (array $serverResults, array $subTask, array $mailTaskBackend): array + { + $result = []; + if ($serverResults[0] == "SUCCESS") { + foreach ($subTask['subTask'] as $subTask => $details) { + + // CN of the main task + $cn = $subTask; + // DN of the main task + $dn = $details['dn']; + + // Update task status for the current $dn + $result[$dn]['statusUpdate'] = $this->gateway->updateTaskStatus($dn, $cn, "2"); + $result[$dn]['mailStatus'] = 'Notification was successfully sent'; + $result[$dn]['updateLastMailExec'] = $this->gateway->updateLastMailExecTime($mailTaskBackend[0]["dn"]); + } + } else { + foreach ($subTask['subTask'] as $subTask => $details) { + + // CN of the main task + $cn = $subTask; + // DN of the main task + $dn = $details['dn']; + + $result[$dn]['statusUpdate'] = $this->gateway->updateTaskStatus($dn, $cn, $serverResults[0]); + $result[$dn]['mailStatus'] = $serverResults; + } + } + + return $result; + } + +} \ No newline at end of file diff --git a/plugins/tasks/Reminder.php b/plugins/tasks/Reminder.php new file mode 100644 index 0000000000000000000000000000000000000000..ca46b55f8de1ba26f451dc9c2469008287cc1c6e --- /dev/null +++ b/plugins/tasks/Reminder.php @@ -0,0 +1,770 @@ +<?php + +class Reminder implements EndpointInterface +{ + + private TaskGateway $gateway; + + public function __construct (TaskGateway $gateway) + { + $this->gateway = $gateway; + } + + /** + * @return array + * Part of the interface of orchestrator plugin to treat GET method + */ + public function processEndPointGet (): array + { + return []; + } + + /** + * @param array|null $data + * @return array + */ + public function processEndPointPost (array $data = NULL): array + { + return []; + } + + /** + * @param array|NULL $data + * @return array + * @throws Exception + */ + public function processEndPointPatch (array $data = NULL): array + { + return $this->processReminder($this->gateway->getObjectTypeTask('reminder')); + } + + /** + * @param array|NULL $data + * @return array + */ + public function processEndPointDelete (array $data = NULL): array + { + return []; + } + + /** + * @param array $reminderSubTasks + * @return array + * @throws Exception + */ + public function processReminder (array $reminderSubTasks): array + { + $result = []; + // It will contain all required reminders to be potentially sent per main task. + $reminders = []; + + foreach ($reminderSubTasks as $task) { + // If the tasks must be treated - status and scheduled - process the sub-tasks + if ($this->gateway->statusAndScheduleCheck($task)) { + + // Retrieve data from the main task + $remindersMainTaskName = $task['fdtasksgranularmaster'][0]; //dn + $remindersMainTask = $this->getRemindersMainTask($remindersMainTaskName); + // remove the count keys + $this->gateway->unsetCountKeys($remindersMainTask); + + // Retrieve email attribute for the monitored members requiring reminding. + $mailOfTheReminded = $this->getEmailFromReminded($task['fdtasksgranulardn'][0]); + + // Generate the mail form with all mail controller requirements + $mailTemplateForm = $this->generateMainTaskMailTemplate($remindersMainTask, $mailOfTheReminded); + + // Get monitored resources + $monitoredResources = $this->getMonitoredResources($remindersMainTask[0]); + + // Case where no supann are monitored nor prolongation desired. (Useless subTask). + if ($monitoredResources['resource'][0] === 'NONE' && $monitoredResources['prolongation'] === 'FALSE') { + // Removal subtask + $result[$task['dn']]['Removed'] = $this->gateway->removeSubTask($task['dn']); + $result[$task['dn']]['Status'] = 'No reminder triggers were found, therefore removing the sub-task!'; + } + + // Case where supann is set monitored but no prolongation desired. + if ($monitoredResources['resource'][0] !== 'NONE' && $monitoredResources['prolongation'] === 'FALSE') { + if ($this->supannAboutToExpire($task['fdtasksgranulardn'][0], $monitoredResources, $task['fdtasksgranularhelper'][0])) { + + // Require to be set for updating the status of the task later on and sent the email. + $reminders[$remindersMainTaskName]['subTask'][$task['cn'][0]]['dn'] = $task['dn']; + $reminders[$remindersMainTaskName]['subTask'][$task['cn'][0]]['uid'] = $task['fdtasksgranulardn'][0]; + // Recipient email form + $reminders[$remindersMainTaskName]['subTask'][$task['cn'][0]]['mail'] = $mailTemplateForm; + + } else { + // Not about to expire, delete subTask + $result[$task['dn']]['Removed'] = $this->gateway->removeSubTask($task['dn']); + $result[$task['dn']]['Status'] = 'No reminder triggers were found, therefore removing the sub-task!'; + } + } + + // Case where supann is set and prolongation is desired. + if ($monitoredResources['resource'][0] !== 'NONE' && $monitoredResources['prolongation'] === 'TRUE') { + if ($this->supannAboutToExpire($task['fdtasksgranulardn'][0], $monitoredResources, $task['fdtasksgranularhelper'][0])) { + // Require to be set for updating the status of the task later on and sent the email. + $reminders[$remindersMainTaskName]['subTask'][$task['cn'][0]]['dn'] = $task['dn']; + $reminders[$remindersMainTaskName]['subTask'][$task['cn'][0]]['uid'] = $task['fdtasksgranulardn'][0]; + + // Create timeStamp expiration for token + $tokenExpire = $this->getTokenExpiration($task['fdtasksgranularhelper'][0], + $remindersMainTask[0]['fdtasksreminderfirstcall'][0], + $remindersMainTask[0]['fdtasksremindersecondcall'][0]); + // Create token for SubTask + $token = $this->generateToken($task['fdtasksgranulardn'][0], $tokenExpire); + // Edit the mailForm with the url link containing the token + $tokenMailTemplateForm = $this->generateTokenUrl($token, $mailTemplateForm, $remindersMainTaskName); + // Recipient email form + $reminders[$remindersMainTaskName]['subTask'][$task['cn'][0]]['mail'] = $tokenMailTemplateForm; + + + } else { + // Not about to expire, delete subTask + $result[$task['dn']]['Removed'] = $this->gateway->removeSubTask($task['dn']); + $result[$task['dn']]['Status'] = 'No reminder triggers were found, therefore removing the sub-task!'; + } + } + + // Case where prolongation is set without supann. + if ($monitoredResources['resource'][0] === 'NONE' && $monitoredResources['prolongation'] === 'TRUE') { + if ($this->posixAboutToExpire($task['fdtasksgranulardn'][0], $task['fdtasksgranularhelper'][0])) { + + // Require to be set for updating the status of the task later on and sent the email. + $reminders[$remindersMainTaskName]['subTask'][$task['cn'][0]]['dn'] = $task['dn']; + $reminders[$remindersMainTaskName]['subTask'][$task['cn'][0]]['uid'] = $task['fdtasksgranulardn'][0]; + + // Create timeStamp expiration for token + $tokenExpire = $this->getTokenExpiration($task['fdtasksgranularhelper'][0], + $remindersMainTask[0]['fdtasksreminderfirstcall'][0], + $remindersMainTask[0]['fdtasksremindersecondcall'][0]); + // Create token for SubTask + $token = $this->generateToken($task['fdtasksgranulardn'][0], $tokenExpire); + // Edit the mailForm with the url link containing the token + $tokenMailTemplateForm = $this->generateTokenUrl($token, $mailTemplateForm, $remindersMainTaskName); + // Recipient email form + $reminders[$remindersMainTaskName]['subTask'][$task['cn'][0]]['mail'] = $tokenMailTemplateForm; + + + } else { + // Not about to expire, delete subTask + $result[$task['dn']]['Removed'] = $this->gateway->removeSubTask($task['dn']); + $result[$task['dn']]['Status'] = 'No reminder triggers were found, therefore removing the sub-task!'; + } + } + } + } + + if (!empty($reminders)) { + $result[] = $this->sendRemindersMail($reminders); + } + + return $result; + } + + /** + * @param $dn + * @param $days + * @return bool + * Note : Compare the date of today and the shadowExpire epoch to see if expiration is soon to happen. + */ + private function posixAboutToExpire ($dn, $days) : bool + { + $result = FALSE; + + $userShadowExpire = $this->retrieveUserPosix($dn); + // Verification if shadowExpire was retrieved + if (!empty($userShadowExpire)) { + // Create the date of today + $today = new DateTime(); + // Create a proper timestamp for verification + $epoch = new DateTime("1970-01-01"); + // Add the shadowExpire days to the epoch + $epoch->add(new DateInterval("P{$userShadowExpire}D")); + + // Get the interval between today and the expiration of shadow expire. + $interval = $today->diff($epoch); + + // Interval can be negative if date is in the past - we make sure it is not in the past by using invert. + if ($interval->days <= $days && $interval->invert == 0) { + $result = TRUE; + } + } + + return $result; + } + + /** + * @param $dn + * @return string + * Note : Simply retrieve shadowExpire attribute for the DN specified. + */ + private function retrieveUserPosix ($dn) : string + { + $result = ''; + $userPosix = $this->gateway->getLdapTasks('(objectClass=shadowAccount)', ['shadowExpire'], + '', $dn); + + // Simply remove key "count" + $this->gateway->unsetCountKeys($userPosix); + + // Removing un-required keys + if (!empty($userPosix[0]['shadowexpire'][0])) { + $result = $userPosix[0]['shadowexpire'][0]; + } + + return $result; + } + + /** + * @param string $token + * @param array $mailTemplateForm + * @param string $taskDN + * @return array + */ + private function generateTokenUrl (string $token, array $mailTemplateForm, string $taskDN): array + { + //Only take the cn of the main task name : + preg_match('/cn=([^,]+),ou=/', $taskDN, $matches); + $taskName = $matches[1]; + + // Remove the API URI + $cleanedUrl = preg_replace('#/rest\.php/v1$#', '', $_ENV['FUSION_DIRECTORY_API_URL']); + $url = $cleanedUrl . '/accountProlongation.php?token=' . $token . '&task=' . $taskName; + + $mailTemplateForm['body'] .= $url; + + return $mailTemplateForm; + } + + /** + * @param int $subTaskCall + * @param int $firstCall + * @param int $secondCall + * @return int + * Note : Simply return the difference between first and second call. (First call can be null). + */ + private function getTokenExpiration (int $subTaskCall, int $firstCall, int $secondCall): int + { + // if firstCall is empty, secondCall is the timestamp expiry for the token. + $result = $secondCall; + + if (!empty($firstCall)) { + // Verification if the subTask is the second reminder or the first reminder. + if ($subTaskCall === $firstCall) { + $result = $firstCall - $secondCall; + } + } + + return $result; + } + + /** + * @param string $userDN + * @param int $timeStamp + * @return string + * @throws Exception + */ + private function generateToken (string $userDN, int $timeStamp): string + { + $token = NULL; + // Salt has been generated with APG. + $salt = '8onOlEsItKond'; + $payload = json_encode($userDN . $salt); + // This allows the token to be different every time. + $time = time(); + + // Create hmac with sha256 alg and the key provided for JWT token signature in ENV. + $token_hmac = hash_hmac("sha256", $time . $payload, $_ENV["SECRET_KEY"], TRUE); + + // We need to have a token allowed to be used within an URL. + $token = $this->base64urlEncode($token_hmac); + + // Save token within LDAP + $this->saveTokenInLdap($userDN, $token, $timeStamp); + + return $token; + } + + /** + * @param string $userDN + * @param string $token + * NOTE : UID is the full DN of the user. (uid=...). + * @param int $days + * @return bool + * @throws Exception + */ + private function saveTokenInLdap (string $userDN, string $token, int $days): bool + { + $result = FALSE; + + $currentTimestamp = time(); + // Calculate the future timestamp by adding days to the current timestamp (We actually adds number of seconds). + $futureTimestamp = $currentTimestamp + ($days * 24 * 60 * 60); + + preg_match('/uid=([^,]+),ou=/', $userDN, $matches); + $uid = $matches[1]; + $dn = 'cn=' . $uid . ',' . 'ou=tokens' . ',' . $_ENV["LDAP_BASE"]; + + $ldap_entry["objectClass"] = ['top', 'fdTokenEntry']; + $ldap_entry["fdTokenUserDN"] = $userDN; + $ldap_entry["fdTokenType"] = 'reminder'; + $ldap_entry["fdToken"] = $token; + $ldap_entry["fdTokenTimestamp"] = $futureTimestamp; + $ldap_entry["cn"] = $uid; + + // set the dn for the token, only take what's between "uid=" and ",ou=" + + + // Verify if token ou branch exists + if (!$this->tokenBranchExist('ou=tokens' . ',' . $_ENV["LDAP_BASE"])) { + // Create the branch + $this->createBranchToken(); + } + + // The user token DN creation + $userTokenDN = 'cn=' . $uid . ',ou=tokens' . ',' . $_ENV["LDAP_BASE"]; + // Verify if a token already exists for specified user and remove it to create new one correctly. + if ($this->tokenBranchExist($userTokenDN)) { + // Remove the user token + $this->removeUserToken($userTokenDN); + } + + // Add token to LDAP for specific UID + try { + $result = ldap_add($this->gateway->ds, $dn, $ldap_entry); // bool returned + } catch (Exception $e) { + echo json_encode(["Ldap Error - Token could not be saved!" => "$e"]); // string returned + exit; + } + + return $result; + } + + /** + * @param $userTokenDN + * @return void + * Note : Simply remove the token for specific user DN + */ + private function removeUserToken ($userTokenDN): void + { + // Add token to LDAP for specific UID + try { + $result = ldap_delete($this->gateway->ds, $userTokenDN); // bool returned + } catch (Exception $e) { + echo json_encode(["Ldap Error - User token could not be removed!" => "$e"]); // string returned + exit; + } + } + + /** + * Create ou=pluginManager LDAP branch + * @throws Exception + */ + protected function createBranchToken (): void + { + try { + ldap_add( + $this->gateway->ds, 'ou=tokens' . ',' . $_ENV["LDAP_BASE"], + [ + 'ou' => 'tokens', + 'objectClass' => 'organizationalUnit', + ] + ); + } catch (Exception $e) { + + echo json_encode(["Ldap Error - Impossible to create the token branch" => "$e"]); // string returned + exit; + } + } + + + /** + * @param string $dn + * @return bool + * Note : Simply inspect if the branch for token is existing. + */ + private function tokenBranchExist (string $dn): bool + { + $result = FALSE; + + try { + $search = ldap_search($this->gateway->ds, $dn, "(objectClass=*)"); + // Check if the search was successful + if ($search) { + // Get the number of entries found + $entries = ldap_get_entries($this->gateway->ds, $search); + + // If entries are found, set result to true + if ($entries["count"] > 0) { + $result = TRUE; + } + } + } catch (Exception $e) { + $result = FALSE; + } + + return $result; + } + + /** + * @param string $text + * @return string + * Note : This come from jwtToken, as it is completely private - it is cloned here for now. + */ + private function base64urlEncode (string $text): string + { + return str_replace(["+", "/", "="], ["A", "B", ""], base64_encode($text)); + } + + /** + * @param string $dn + * @return string + * Note : return the mail attribute from gosaMail objectclass. + */ + private function getEmailFromReminded (string $dn): string + { + // in case the DN do not have an email set. - Return string FALSE. + $result = "FALSE"; + $email = $this->gateway->getLdapTasks('(objectClass=gosaMailAccount)', ['mail'], + '', $dn); + // Simply remove key "count" + $this->gateway->unsetCountKeys($email); + + // Removing un-required keys (ldap return array with count and 0). + if (!empty($email[0]['mail'][0])) { + $result = $email[0]['mail'][0]; + } + + return $result; + } + + /** + * @param $task + * @return bool + * Note : Verify the account status of the DN with the requirements of main tasks. + */ + private function supannAboutToExpire (string $dn, array $monitoredResources, int $days): bool + { + $result = FALSE; + + // Search the DN for supannRessourceEtatDate (With DATE) + $supannResources = $this->retrieveSupannResources($dn); + // Get the matching resource (without date) + $matchedResource = $this->verifySupannState($monitoredResources, $supannResources); + + if ($matchedResource) { + // verify + $DnSupannDateObject = $this->retrieveDateFromSupannResourceState($supannResources['supannressourceetatdate'], $matchedResource); + //Verification if the time is lower or equal than the reminder time. + if ($DnSupannDateObject !== FALSE) { + $today = new DateTime(); + $interval = $today->diff($DnSupannDateObject); + + // Interval can be negative if date is in the past - we make sure it is not in the past by using invert. + if ($interval->days <= $days && $interval->invert == 0) { + $result = TRUE; + } + } + } + + return $result; + } + + /** + * @param $dn + * @return array + * Note : Simply return supann resource array from the specific passed DN. + */ + private function retrieveSupannResources ($dn): array + { + $supannResources = []; + $supannResources = $this->gateway->getLdapTasks('(objectClass=supannPerson)', ['supannRessourceEtatDate', 'supannRessourceEtat'], + '', $dn); + // Simply remove key "count" + $this->gateway->unsetCountKeys($supannResources); + + // Removing un-required keys + if (!empty($supannResources)) { + $supannResources = $supannResources[0]; + } + + return $supannResources; + + } + + + /** + * Get the monitored resources for reminder to be activated. + * @param array $remindersMainTask + * @return array + */ + private function getMonitoredResources (array $remindersMainTask): array + { + $monitoredResourcesArray = [ + 'resource' => $remindersMainTask['fdtasksreminderresource'], + 'state' => $remindersMainTask['fdtasksreminderstate'], + 'subState' => $remindersMainTask['fdtasksremindersubstate'] ?? NULL + ]; + + // Boolean returned by ldap is a string. + if (isset($remindersMainTask['fdtasksreminderaccountprolongation'][0]) && $remindersMainTask['fdtasksreminderaccountprolongation'][0] === 'TRUE') { + // Add the potential next resources states to the array + if (isset($remindersMainTask['fdtasksremindernextresource'])) { + + $monitoredResourcesArray['nextResource'] = $remindersMainTask['fdtasksremindernextresource']; + $monitoredResourcesArray['nextState'] = $remindersMainTask['fdtasksremindernextstate']; + $monitoredResourcesArray['nextSubState'] = $remindersMainTask['fdtasksremindernextsubstate'] ?? NULL; + } + // Posix attributes + $monitoredResourcesArray['fdTasksReminderPosix'] = $remindersMainTask['fdtasksreminderposix'] ?? FALSE; + + } + + // For development logic, add the prolongation attribute. It will be checked later in the logic process. + $monitoredResourcesArray['prolongation'] = $remindersMainTask['fdtasksreminderaccountprolongation'][0] ?? FALSE; + + return $monitoredResourcesArray; + } + + + /** + * @param array $reminderSupann + * @param array $dnSupann + * @return string + * Note : Create the supann format and check for a match. + */ + private function verifySupannState (array $reminderSupann, array $dnSupann): string + { + // Result will contain the supann resource matching. + $result = ''; + + //Construct the reminder Supann Resource State as string + if (!empty($reminderSupann['subState'][0])) { + $monitoredSupannState = '{' . $reminderSupann['resource'][0] . '}' . $reminderSupann['state'][0] . ':' . $reminderSupann['subState'][0]; + } else { + $monitoredSupannState = '{' . $reminderSupann['resource'][0] . '}' . $reminderSupann['state'][0]; + } + + if (!empty($dnSupann['supannressourceetat'])) { + // Simply iterate within the resource available till a match is found. + foreach ($dnSupann['supannressourceetat'] as $resource) { + if ($monitoredSupannState === $resource) { + $result = $resource; + break; + } + } + } + + return $result; + } + + /** + * @param array $supannEtatDate + * @param string $resource + * @return DateTime|false + * Note : Simply transform string date of supann to a dateTime object. + * Can return bool (false) or dateTime object. + */ + private function retrieveDateFromSupannResourceState (array $supannEtatDate, string $resource) + { + $dateString = NULL; + $matchFound = NULL; // Variable to store the match if found + + // Create a regex pattern to match the exact resource at the beginning, followed by ":" or ":::". + $pattern = '/^' . preg_quote($resource, '/') . '(:|:::)?.*/'; + + foreach ($supannEtatDate as $resourceWithDate) { + if (preg_match($pattern, $resourceWithDate)) { + $matchFound = $resourceWithDate; + break; // Stop once a match is found + } + } + + // Simply take the last 8 digit + preg_match('/(\d{8})$/', $matchFound, $matches); + + if (!empty($matches)) { + $dateString = $matches[0]; + } + + return DateTime::createFromFormat('Ymd', $dateString); + } + + /** + * @param $array + * @return array + * Note : simply return all values of a multi-dimensional array. + */ + public function getArrayValuesRecursive ($array) + { + $values = []; + foreach ($array as $value) { + if (is_array($value)) { + // If value is an array, merge its values recursively + $values = array_merge($values, $this->getArrayValuesRecursive($value)); + } else { + // If value is not an array, add it to the result + $values[] = $value; + } + } + return $values; + } + + /** + * @param string $mainTaskDn + * @return array + */ + public function getRemindersMainTask (string $mainTaskDn): array + { + // Retrieve data from the main Reminder task + return $this->gateway->getLdapTasks( '(objectClass=fdTasksReminder)', ['fdTasksReminderListOfRecipientsMails', + 'fdTasksReminderResource', 'fdTasksReminderState', 'fdTasksReminderPosix', 'fdTasksReminderMailTemplate', + 'fdTasksReminderSupannNewEndDate', 'fdTasksReminderRecipientsMembers', 'fdTasksReminderEmailSender', + 'fdTasksReminderAccountProlongation', 'fdTasksReminderMembers', 'fdTasksReminderNextResource', + 'fdTasksReminderNextState', 'fdTasksReminderNextSubState', 'fdTasksReminderSubState', 'fdTasksReminderFirstCall', 'fdTasksReminderSecondCall'], '', $mainTaskDn); + } + + /** + * @param array $mainTask + * @param string $remindedEmail + * @return array + * Note : Simply generate the email to be sent as reminder. + * Note 2 : The boolean is created to generate the token and is only sent to reminded. Not recipients. + */ + private function generateMainTaskMailTemplate (array $mainTask, string $remindedEmail): array + { + // Generate email configuration for each result of subtasks having the same main task. + $sender = $mainTask[0]['fdtasksreminderemailsender'][0]; + $mailTemplateName = $mainTask[0]['fdtasksremindermailtemplate'][0]; + + $mailInfos = $this->gateway->getLdapTasks("(|(objectClass=fdMailTemplate)(objectClass=fdMailAttachments))", [], $mailTemplateName); + $mailContent = $mailInfos[0]; + + // If no forward-to mail recipients is set, simply send the reminder to the monitored members. + if (!empty($mainTask[0]["fdtasksreminderlistofrecipientsmails"])) { + $recipients = array_merge($mainTask[0]["fdtasksreminderlistofrecipientsmails"], [$remindedEmail]); + $this->gateway->unsetCountKeys($recipients); + + // There is no reason to send an email twice to the same person. Render the array unique. + $recipients = array_unique($recipients); + } else { + $recipients = $remindedEmail; + } + + // Render the array unique. + + // Set the reminder array with all required variable for all sub-tasks of same main task origin. + $mailForm['setFrom'] = $sender; + $mailForm['recipients'] = $recipients; + $mailForm['body'] = $mailContent["fdmailtemplatebody"][0]; + $mailForm['signature'] = $mailContent["fdmailtemplatesignature"][0] ?? NULL; + $mailForm['subject'] = $mailContent["fdmailtemplatesubject"][0]; + $mailForm['receipt'] = $mailContent["fdmailtemplatereadreceipt"][0]; + + return $mailForm; + } + + /** + * @param array $reminders + * @return array + * Note : Collect information and send reminder email. + */ + protected function sendRemindersMail (array $reminders): array + { + $result = []; + // Re-use of the same mail processing template logic + $fdTasksConf = $this->gateway->getLdapTasks( + "(objectClass=fdTasksConf)", + ["fdTasksConfLastExecTime", "fdTasksConfIntervalEmails", "fdTasksConfMaxEmails"] + ); + $maxMailsConfig = $fdTasksConf[0]["fdtasksconfmaxemails"][0] ?? 50; + + /* + Increment var starts a zero and added values will be the number of recipients per main tasks, as one mail is + sent per main task. + */ + $maxMailsIncrement = 0; + + // Each reminders + foreach ($reminders as $reminder) { + // Each main task reminder + foreach ($reminder as $reminderItem) { + // Each subTask reminder + foreach ($reminderItem as $mailDetails) { + + // It is not impossible that only one recipient exist, therefore it won't be an array. + if (!is_array($mailDetails['mail']['recipients'])) { + // Simply transform the string into an array + $mailDetails['mail']['recipients'] = [$mailDetails['mail']['recipients']]; + } + $numberOfRecipients = count($mailDetails['mail']['recipients']); + + $mail_controller = new \FusionDirectory\Mail\MailLib( + $mailDetails['mail']['setFrom'], + NULL, + $mailDetails['mail']['recipients'], + $mailDetails['mail']['body'], + $mailDetails['mail']['signature'], + $mailDetails['mail']['subject'], + $mailDetails['mail']['receipt'], + NULL + ); + + $mailSentResult = $mail_controller->sendMail(); + // Here we incremented as well the counter of spam to the backend. + $result[] = $this->processMailResponseAndUpdateTasks($mailSentResult, $reminder, $fdTasksConf); + + // Verification anti-spam max mails to be sent and quit loop if matched. + $maxMailsIncrement += $numberOfRecipients; + if ($maxMailsIncrement == $maxMailsConfig) { + break; + } + } + } + } + + return $result; + } + + /** + * @param array $serverResults + * @param array $subTask + * @param array $mailTaskBackend + * @return array + * Note : + */ + protected function processMailResponseAndUpdateTasks (array $serverResults, array $subTask, array $mailTaskBackend): array + { + $result = []; + if ($serverResults[0] == "SUCCESS") { + foreach ($subTask['subTask'] as $subTask => $details) { + + // CN of the main task + $cn = $subTask; + // DN of the main task + $dn = $details['dn']; + + // Update task status for the current $dn + $result[$dn]['statusUpdate'] = $this->gateway->updateTaskStatus($dn, $cn, "2"); + $result[$dn]['mailStatus'] = 'reminder was successfully sent'; + $result[$dn]['updateLastMailExec'] = $this->gateway->updateLastMailExecTime($mailTaskBackend[0]["dn"]); + } + } else { + foreach ($subTask['subTask'] as $subTask => $details) { + + // CN of the main task + $cn = $subTask; + // DN of the main task + $dn = $details['dn']; + + $result[$dn]['statusUpdate'] = $this->gateway->updateTaskStatus($dn, $cn, $serverResults[0]); + $result[$dn]['mailStatus'] = $serverResults; + } + } + + return $result; + } + +} \ No newline at end of file