From 82fe336f34b60a38015fc2944ce8fb71bdd0f996 Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Wed, 24 Jan 2024 19:11:29 +0530 Subject: [PATCH 1/9] Fix issue - session booking notifications --- conditions/session/classes/conditionform.php | 4 ++-- conditions/session/version.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conditions/session/classes/conditionform.php b/conditions/session/classes/conditionform.php index ff04271..187ec5c 100644 --- a/conditions/session/classes/conditionform.php +++ b/conditions/session/classes/conditionform.php @@ -94,12 +94,12 @@ public function is_user_completed($instancedata, $userid, \completion_info $comp global $DB; // Get the notification suppres module ids. - $additional = $instancedata->conditions['session'] ?? []; + $additional = $instancedata->condition['session'] ?? []; $modules = $additional['modules'] ?? ''; if (!empty($modules)) { $result = []; - $sql = "SELECT * FROM {facetoface_signups} f2f_su + $sql = "SELECT count(*) FROM {facetoface_signups} f2f_su JOIN {facetoface_sessions} f2f_ss ON f2f_ss.id = f2f_su.sessionid WHERE f2f_ss.facetoface = :f2fid AND f2f_su.userid = :userid"; diff --git a/conditions/session/version.php b/conditions/session/version.php index ef9cb69..e860d20 100644 --- a/conditions/session/version.php +++ b/conditions/session/version.php @@ -24,5 +24,5 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'pulsecondition_session'; -$plugin->version = 2023080203; +$plugin->version = 2023080204; $plugin->dependencies = array('mod_facetoface' => 2021113000); // Dependencies set for the session module "Face to Face". From 7dd591f814525407a8d7073c68d7bdc6c0fe83ad Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Mon, 5 Feb 2024 19:09:00 +0530 Subject: [PATCH 2/9] Improve session signup/cancellation based schedule. - Fix PLS-698 --- conditions/session/classes/conditionform.php | 130 ++++++++++++++++++- conditions/session/db/events.php | 5 +- conditions/session/lib.php | 81 ++++++++++++ conditions/session/version.php | 2 +- 4 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 conditions/session/lib.php diff --git a/conditions/session/classes/conditionform.php b/conditions/session/classes/conditionform.php index 187ec5c..73cfe6f 100644 --- a/conditions/session/classes/conditionform.php +++ b/conditions/session/classes/conditionform.php @@ -101,9 +101,13 @@ public function is_user_completed($instancedata, $userid, \completion_info $comp $sql = "SELECT count(*) FROM {facetoface_signups} f2f_su JOIN {facetoface_sessions} f2f_ss ON f2f_ss.id = f2f_su.sessionid - WHERE f2f_ss.facetoface = :f2fid AND f2f_su.userid = :userid"; + JOIN {facetoface_signups_status} f2f_sts ON f2f_su.id = f2f_sts.signupid + WHERE f2f_ss.facetoface = :f2fid AND f2f_su.userid = :userid + AND f2f_sts.superceded != 1 AND f2f_sts.statuscode = :booked"; + + $existingsignup = $DB->count_records_sql($sql, array( + 'f2fid' => $modules, 'userid' => $userid, 'booked' => MDL_F2F_STATUS_BOOKED)); - $existingsignup = $DB->count_records_sql($sql, array('f2fid' => $modules, 'userid' => $userid)); return ($existingsignup) ? true : false; } // Not configured any session modules. @@ -218,4 +222,126 @@ public static function get_session_data($face2faceid, $userid) { ), 0, 1); return $existingsignup; } + + /** + * Prepare the schedule for the user signup to the session. + * + * Gets the session from the param, and fetch the list of notification instance configured with this session. + * Filters the users list selected to signup to the session with signup users and its status code. + * + * Then all the notification instances are triggered for the filtered users. + * + * @param int|null $instanceid Face to face instance id. + * @return void + */ + public static function prepare_session_signup_schedule(?int $instanceid=null) { + global $PAGE, $DB; + + // Current session id. + $sessionid = required_param('s', PARAM_INT); + + // Get the list of signup users. + $session = facetoface_get_session($sessionid); + $instanceid = $instanceid ?: $session->facetoface; + + $potentialuserselector = new \facetoface_candidate_selector('addselect', array('sessionid' => $session->id, 'courseid' => $PAGE->course->id)); + $addusers = optional_param_array($potentialuserselector->get_name(), array(), PARAM_INT); + + list($insql, $inparams) = $DB->get_in_or_equal($addusers, SQL_PARAMS_NAMED, 'f2fu'); + $params = ['booked' => MDL_F2F_STATUS_BOOKED, 'sessionid' => $sessionid]; + + // Filter the users based on the signup status. + $users = $DB->get_fieldset_sql(" + SELECT DISTINCT f2f_su.userid FROM {facetoface_signups} f2f_su + JOIN {facetoface_signups_status} f2f_sts ON f2f_su.id = f2f_sts.signupid + WHERE f2f_su.sessionid=:sessionid AND f2f_sts.statuscode = :booked AND f2f_su.userid $insql + GROUP BY f2f_su.userid + ", $params + $inparams); + + // Self condition instance. + $condition = new self(); + + // Fetch the session notifications uses this session signup. + $notifications = self::get_session_notifications($instanceid); + + foreach ($notifications as $notification) { + // Get the notification suppres module ids. + $additional = $notification->additional ? json_decode($notification->additional, true) : ''; + $modules = $additional['modules'] ?? ''; + + if (!empty($modules)) { + + $session = $DB->get_record('facetoface_sessions_dates', array('sessionid' => $sessionid)); + // Trigger all the instance for notifications. + foreach ($users as $userid) { + $condition->trigger_instance($notification->instanceid, $userid, $session->timestart); + } + } + + } + } + + /** + * Remove the schedule for the user removed from the session. + * + * Gets the session from the param, and fetch the list of notification instance configured with this session. + * Filters the users list selected to remove signup from the session. + * + * Then all the notification instances are triggered for the filtered users. this will make the schedule on hold. + * + * @param int|null $instanceid Face to face instance id. + * @return void + */ + public static function remove_session_signup_schedule(?int $instanceid=null) { + global $PAGE, $DB; + + // Current session id. + $sessionid = required_param('s', PARAM_INT); + + // Get the list of signup users. + $session = facetoface_get_session($sessionid); + $instanceid = $instanceid ?: $session->facetoface; + + $potentialuserselector = new \facetoface_candidate_selector('removeselect', array('sessionid' => $session->id, 'courseid' => $PAGE->course->id)); + $removeusers = optional_param_array($potentialuserselector->get_name(), array(), PARAM_INT); + + list($insql, $inparams) = $DB->get_in_or_equal($removeusers, SQL_PARAMS_NAMED, 'f2fu'); + $params = ['booked' => MDL_F2F_STATUS_BOOKED, 'sessionid' => $sessionid]; + + // Filter the users based on the signup status. + $users = $DB->get_fieldset_sql(" + SELECT DISTINCT f2f_su.userid FROM {facetoface_signups} f2f_su + JOIN {facetoface_signups_status} f2f_sts ON f2f_su.id = f2f_sts.signupid + WHERE f2f_su.sessionid=:sessionid AND f2f_sts.statuscode = :booked AND f2f_su.userid $insql + GROUP BY f2f_su.userid + ", $params + $inparams); + + // Self condition instance. + $condition = new self(); + + // Fetch the session notifications uses this session signup. + $notifications = self::get_session_notifications($instanceid); + + foreach ($notifications as $notification) { + // Get the notification suppres module ids. + $additional = $notification->additional ? json_decode($notification->additional, true) : ''; + $modules = $additional['modules'] ?? ''; + + if (!empty($modules)) { + + $session = $DB->get_record('facetoface_sessions_dates', array('sessionid' => $sessionid)); + // Trigger all the instance for notifications. + foreach ($removeusers as $userid) { + if (isset($users[$userid])) { + continue; + } + // Trigger the instance will verify the user compleiton status of session signup. + // In this case user is cancelled from the session, so the schedule status will be updated to on-hold. + $condition->trigger_instance($notification->instanceid, $userid, $session->timestart); + } + } + + } + + } } diff --git a/conditions/session/db/events.php b/conditions/session/db/events.php index aff3b41..74dbd09 100644 --- a/conditions/session/db/events.php +++ b/conditions/session/db/events.php @@ -33,5 +33,8 @@ 'eventname' => '\mod_facetoface\event\signup_failed', 'callback' => '\pulsecondition_session\conditionform::signup_success', ), - + array( + 'eventname' => '\mod_facetoface\event\cancel_booking', + 'callback' => '\pulsecondition_session\conditionform::signup_success', + ), ]; diff --git a/conditions/session/lib.php b/conditions/session/lib.php new file mode 100644 index 0000000..644f027 --- /dev/null +++ b/conditions/session/lib.php @@ -0,0 +1,81 @@ +. + +/** + * Pulse condition session common functions to observe moodle default hooks + * + * @package pulsecondition_session + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + /** + * Name of the session module. + */ +define('PULSE_SESSION_MOD', 'facetoface'); + +/** + * Type of the edit attendees page. + */ +define('PULSE_SESSION_MOD_EDITPAGEID', 'mod-facetoface-editattendees'); + +/** + * Extended the course navigation to observe the user add/remove from session from the backend by admin/managers. + * Verify the add param and verifies the page is session edit attendees page. Then triggers the schedule preparation. + * + * @param navigation_node $navigation + * @param stdClass $course Course info data. + * @param \context $context + * @return void + */ +function pulsecondition_session_extend_navigation_course(navigation_node $navigation, stdClass $course, $context) { + global $PAGE, $SCRIPT; + + // Verify the page is facetoface edit attendees page and the admin/teachers added user to signup from backend. + // Trigger the pulse to get the list of new user signup in this session and create a schedule for those users. + if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) { + + if ($PAGE->pagetype == PULSE_SESSION_MOD_EDITPAGEID && $PAGE->cm->modname == PULSE_SESSION_MOD) { + \pulsecondition_session\conditionform::prepare_session_signup_schedule($PAGE->cm->instance); + return true; + } + + // When the error is raised during the signup, face to face throw exception, + // This exception prevents the above schedule content to run. + // Throw exception resets the PAGE urls, cm info, for the reason. + // In this case the page is set as site index and the course is not frontpage but the current file path is facetoface. + if ($PAGE->pagetype == 'site-index' && $PAGE->course->id != SITEID && $SCRIPT == '/mod/facetoface/editattendees.php') { + \pulsecondition_session\conditionform::prepare_session_signup_schedule($PAGE->cm->instance); + return true; + } + } + + // Verify the page is facetoface edit attendees page and the admin/teachers added user to signup from backend. + // Trigger the pulse to get the list of new user signup in this session and create a schedule for those users. + if (optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) { + + if ($PAGE->pagetype == PULSE_SESSION_MOD_EDITPAGEID && $PAGE->cm->modname == PULSE_SESSION_MOD) { + \pulsecondition_session\conditionform::remove_session_signup_schedule($PAGE->cm->instance); + return true; + } + + if ($PAGE->pagetype == 'site-index' && $PAGE->course->id != SITEID && $SCRIPT == '/mod/facetoface/editattendees.php') { + \pulsecondition_session\conditionform::remove_session_signup_schedule($PAGE->cm->instance); + return true; + } + + } +} diff --git a/conditions/session/version.php b/conditions/session/version.php index e860d20..b7a07d5 100644 --- a/conditions/session/version.php +++ b/conditions/session/version.php @@ -24,5 +24,5 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'pulsecondition_session'; -$plugin->version = 2023080204; +$plugin->version = 2023080205; $plugin->dependencies = array('mod_facetoface' => 2021113000); // Dependencies set for the session module "Face to Face". From e3c3988795a76c8ff4b663de9aa1376794c91255 Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Mon, 5 Feb 2024 19:22:45 +0530 Subject: [PATCH 3/9] Update the capability to access the automation navigation node. - Fix PLS-696 --- lib.php | 4 ++-- version.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib.php b/lib.php index 22e3390..ee4c8b7 100644 --- a/lib.php +++ b/lib.php @@ -646,14 +646,14 @@ function mod_pulse_inplace_editable($itemtype, $itemid, $newvalue) { * * @param navigation_node $navigation * @param stdClass $course - * @param context_course $context + * @param \context $context * @return void */ function mod_pulse_extend_navigation_course(navigation_node $navigation, stdClass $course, $context) { global $PAGE; $addnode = $context->contextlevel === CONTEXT_COURSE; - $addnode = $addnode && has_capability('gradereport/grader:view', $context); // TODO: Custom capability. + $addnode = $addnode && has_capability('mod/pulse:addtemplateinstance', $context); if ($addnode) { $id = $context->instanceid; $url = new moodle_url('/mod/pulse/automation/instances/list.php', [ diff --git a/version.php b/version.php index 7ef8a36..398b948 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_pulse'; -$plugin->version = 2023100702; +$plugin->version = 2023100703; $plugin->requires = 2020061500; // Requires Moodle 3.90. $plugin->release = 'v2.0'; $plugin->maturity = MATURITY_RC; From 9d3fce7976fd6d8f344a6458aa1457986b81dccc Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Mon, 5 Feb 2024 20:00:45 +0530 Subject: [PATCH 4/9] Moodle CI improve --- conditions/session/classes/conditionform.php | 26 ++++++++++++++------ conditions/session/lib.php | 10 +++++--- conditions/session/version.php | 2 +- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/conditions/session/classes/conditionform.php b/conditions/session/classes/conditionform.php index 73cfe6f..95747e0 100644 --- a/conditions/session/classes/conditionform.php +++ b/conditions/session/classes/conditionform.php @@ -103,10 +103,12 @@ public function is_user_completed($instancedata, $userid, \completion_info $comp JOIN {facetoface_sessions} f2f_ss ON f2f_ss.id = f2f_su.sessionid JOIN {facetoface_signups_status} f2f_sts ON f2f_su.id = f2f_sts.signupid WHERE f2f_ss.facetoface = :f2fid AND f2f_su.userid = :userid - AND f2f_sts.superceded != 1 AND f2f_sts.statuscode = :booked"; + AND f2f_sts.superceded != 1 + AND f2f_sts.statuscode >= :code AND f2f_sts.statuscode < :statuscode"; $existingsignup = $DB->count_records_sql($sql, array( - 'f2fid' => $modules, 'userid' => $userid, 'booked' => MDL_F2F_STATUS_BOOKED)); + 'f2fid' => $modules, 'userid' => $userid, + 'code' => MDL_F2F_STATUS_REQUESTED, 'statuscode' => MDL_F2F_STATUS_NO_SHOW)); return ($existingsignup) ? true : false; } @@ -244,17 +246,21 @@ public static function prepare_session_signup_schedule(?int $instanceid=null) { $session = facetoface_get_session($sessionid); $instanceid = $instanceid ?: $session->facetoface; - $potentialuserselector = new \facetoface_candidate_selector('addselect', array('sessionid' => $session->id, 'courseid' => $PAGE->course->id)); + $potentialuserselector = new \facetoface_candidate_selector('addselect', array( + 'sessionid' => $session->id, 'courseid' => $PAGE->course->id)); + // Users to signup to the session. $addusers = optional_param_array($potentialuserselector->get_name(), array(), PARAM_INT); list($insql, $inparams) = $DB->get_in_or_equal($addusers, SQL_PARAMS_NAMED, 'f2fu'); - $params = ['booked' => MDL_F2F_STATUS_BOOKED, 'sessionid' => $sessionid]; + $params = ['code' => MDL_F2F_STATUS_REQUESTED, 'statuscode' => MDL_F2F_STATUS_NO_SHOW, 'sessionid' => $sessionid]; // Filter the users based on the signup status. $users = $DB->get_fieldset_sql(" SELECT DISTINCT f2f_su.userid FROM {facetoface_signups} f2f_su JOIN {facetoface_signups_status} f2f_sts ON f2f_su.id = f2f_sts.signupid - WHERE f2f_su.sessionid=:sessionid AND f2f_sts.statuscode = :booked AND f2f_su.userid $insql + WHERE f2f_su.sessionid=:sessionid + AND f2f_sts.statuscode >= :code AND f2f_sts.statuscode < :statuscode + AND f2f_su.userid $insql GROUP BY f2f_su.userid ", $params + $inparams); @@ -279,6 +285,7 @@ public static function prepare_session_signup_schedule(?int $instanceid=null) { } } + } /** @@ -302,17 +309,20 @@ public static function remove_session_signup_schedule(?int $instanceid=null) { $session = facetoface_get_session($sessionid); $instanceid = $instanceid ?: $session->facetoface; - $potentialuserselector = new \facetoface_candidate_selector('removeselect', array('sessionid' => $session->id, 'courseid' => $PAGE->course->id)); + $potentialuserselector = new \facetoface_candidate_selector('removeselect', array( + 'sessionid' => $session->id, 'courseid' => $PAGE->course->id)); $removeusers = optional_param_array($potentialuserselector->get_name(), array(), PARAM_INT); list($insql, $inparams) = $DB->get_in_or_equal($removeusers, SQL_PARAMS_NAMED, 'f2fu'); - $params = ['booked' => MDL_F2F_STATUS_BOOKED, 'sessionid' => $sessionid]; + $params = ['code' => MDL_F2F_STATUS_REQUESTED, 'statuscode' => MDL_F2F_STATUS_NO_SHOW, 'sessionid' => $sessionid]; // Filter the users based on the signup status. $users = $DB->get_fieldset_sql(" SELECT DISTINCT f2f_su.userid FROM {facetoface_signups} f2f_su JOIN {facetoface_signups_status} f2f_sts ON f2f_su.id = f2f_sts.signupid - WHERE f2f_su.sessionid=:sessionid AND f2f_sts.statuscode = :booked AND f2f_su.userid $insql + WHERE f2f_su.sessionid=:sessionid + AND f2f_sts.statuscode >= :code AND f2f_sts.statuscode < :statuscode + AND f2f_su.userid $insql GROUP BY f2f_su.userid ", $params + $inparams); diff --git a/conditions/session/lib.php b/conditions/session/lib.php index 644f027..668158f 100644 --- a/conditions/session/lib.php +++ b/conditions/session/lib.php @@ -46,7 +46,8 @@ function pulsecondition_session_extend_navigation_course(navigation_node $naviga // Verify the page is facetoface edit attendees page and the admin/teachers added user to signup from backend. // Trigger the pulse to get the list of new user signup in this session and create a schedule for those users. - if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) { + $addselect = optional_param_array('addselect', array(), PARAM_INT); + if (optional_param('add', false, PARAM_BOOL) && !empty($addselect)) { if ($PAGE->pagetype == PULSE_SESSION_MOD_EDITPAGEID && $PAGE->cm->modname == PULSE_SESSION_MOD) { \pulsecondition_session\conditionform::prepare_session_signup_schedule($PAGE->cm->instance); @@ -58,14 +59,15 @@ function pulsecondition_session_extend_navigation_course(navigation_node $naviga // Throw exception resets the PAGE urls, cm info, for the reason. // In this case the page is set as site index and the course is not frontpage but the current file path is facetoface. if ($PAGE->pagetype == 'site-index' && $PAGE->course->id != SITEID && $SCRIPT == '/mod/facetoface/editattendees.php') { - \pulsecondition_session\conditionform::prepare_session_signup_schedule($PAGE->cm->instance); + \pulsecondition_session\conditionform::prepare_session_signup_schedule(); return true; } } // Verify the page is facetoface edit attendees page and the admin/teachers added user to signup from backend. // Trigger the pulse to get the list of new user signup in this session and create a schedule for those users. - if (optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) { + $removeusers = optional_param_array('removeselect', array(), PARAM_INT); + if (optional_param('remove', false, PARAM_BOOL) && !empty($removeusers)) { if ($PAGE->pagetype == PULSE_SESSION_MOD_EDITPAGEID && $PAGE->cm->modname == PULSE_SESSION_MOD) { \pulsecondition_session\conditionform::remove_session_signup_schedule($PAGE->cm->instance); @@ -73,7 +75,7 @@ function pulsecondition_session_extend_navigation_course(navigation_node $naviga } if ($PAGE->pagetype == 'site-index' && $PAGE->course->id != SITEID && $SCRIPT == '/mod/facetoface/editattendees.php') { - \pulsecondition_session\conditionform::remove_session_signup_schedule($PAGE->cm->instance); + \pulsecondition_session\conditionform::remove_session_signup_schedule(); return true; } diff --git a/conditions/session/version.php b/conditions/session/version.php index b7a07d5..b169787 100644 --- a/conditions/session/version.php +++ b/conditions/session/version.php @@ -24,5 +24,5 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'pulsecondition_session'; -$plugin->version = 2023080205; +$plugin->version = 2023080206; $plugin->dependencies = array('mod_facetoface' => 2021113000); // Dependencies set for the session module "Face to Face". From dd9f79fdd39b5b116d7897e9d92325b198c9c42e Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Tue, 6 Feb 2024 12:03:00 +0530 Subject: [PATCH 5/9] Improved course completion condition. - Fix PLS-711, PLS-709 --- actions/notification/classes/notification.php | 4 ++-- classes/automation/helper.php | 3 +++ conditions/course/classes/conditionform.php | 5 +++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/actions/notification/classes/notification.php b/actions/notification/classes/notification.php index c94bdfa..a45c6d4 100644 --- a/actions/notification/classes/notification.php +++ b/actions/notification/classes/notification.php @@ -531,7 +531,7 @@ public function create_schedule_forinstance($newenrolment=false) { $users = $this->get_users_withroles($roles, $context); foreach ($users as $userid => $user) { $suppressreached = notify_users::is_suppress_reached( - $this->notificationdata, $userid, $this->instancedata->course, null); + $this->notificationdata, $user->id, $this->instancedata->course, null); if ($suppressreached) { continue; } @@ -730,7 +730,7 @@ protected function get_users_withroles(array $roles, $context, $childuserid=null list($insql, $inparams) = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, 'rle'); // TODO: Define user fields, never get entire fields. - $rolesql = "SELECT DISTINCT u.id, u.*, ra.roleid FROM {role_assignments} ra + $rolesql = "SELECT u.*, ra.id, ra.roleid FROM {role_assignments} ra JOIN {user} u ON u.id = ra.userid JOIN {role} r ON ra.roleid = r.id LEFT JOIN {role_names} rn ON (rn.contextid = :ctxid AND rn.roleid = r.id) "; diff --git a/classes/automation/helper.php b/classes/automation/helper.php index 14b7cdc..c0d0633 100644 --- a/classes/automation/helper.php +++ b/classes/automation/helper.php @@ -453,6 +453,9 @@ public function timemanagement_details(string $var, \stdClass $course, int $user // Upcoming event dates. if ($var == 'eventdates') { + // Calender lib inclusion. + require_once($CFG->dirroot.'/calendar/lib.php'); + $calendar = \calendar_information::create(time(), $course->id, null); list($data, $template) = calendar_get_view($calendar, 'upcoming_mini'); $final = isset($data->events) ? array_map(function($event) { diff --git a/conditions/course/classes/conditionform.php b/conditions/course/classes/conditionform.php index edda20e..03d196c 100644 --- a/conditions/course/classes/conditionform.php +++ b/conditions/course/classes/conditionform.php @@ -84,7 +84,9 @@ public static function course_completed($eventdata) { $data = $eventdata->get_data(); $courseid = $data['courseid']; - $relateduserid = $data['userid']; + // Use the related user id, instead of userid. + // When the course is completed via cron then the event uses the admin user as event user. + $relateduserid = $data['relateduserid']; // Trigger the instances, this will trigger its related actions for this user. $like = $DB->sql_like('pat.triggerconditions', ':value'); @@ -99,7 +101,6 @@ public static function course_completed($eventdata) { $instances = $DB->get_records_sql($sql, $params); foreach ($instances as $key => $instance) { - // TODO: Condition status check. $condition = (new self())->trigger_instance($instance->instanceid, $relateduserid); } From e108ef2ad8688d16b877dc4f51574000c4d8b84d Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Tue, 6 Feb 2024 12:03:40 +0530 Subject: [PATCH 6/9] Version increased --- version.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.php b/version.php index 398b948..62b9467 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_pulse'; -$plugin->version = 2023100703; +$plugin->version = 2023100704; $plugin->requires = 2020061500; // Requires Moodle 3.90. $plugin->release = 'v2.0'; $plugin->maturity = MATURITY_RC; From 30829f42aaaa71715dc0568fd10594abb3c6cece Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Wed, 14 Feb 2024 11:47:46 +0530 Subject: [PATCH 7/9] Fetch users list method improved --- actions/notification/classes/notification.php | 2 +- classes/automation/instances.php | 1 + classes/plugininfo/pulsecondition.php | 22 +++++++++---------- version.php | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/actions/notification/classes/notification.php b/actions/notification/classes/notification.php index a45c6d4..11b0599 100644 --- a/actions/notification/classes/notification.php +++ b/actions/notification/classes/notification.php @@ -730,7 +730,7 @@ protected function get_users_withroles(array $roles, $context, $childuserid=null list($insql, $inparams) = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, 'rle'); // TODO: Define user fields, never get entire fields. - $rolesql = "SELECT u.*, ra.id, ra.roleid FROM {role_assignments} ra + $rolesql = "SELECT ra.id as assignid, u.*, ra.roleid FROM {role_assignments} ra JOIN {user} u ON u.id = ra.userid JOIN {role} r ON ra.roleid = r.id LEFT JOIN {role_names} rn ON (rn.contextid = :ctxid AND rn.roleid = r.id) "; diff --git a/classes/automation/instances.php b/classes/automation/instances.php index 13493e2..abc5867 100644 --- a/classes/automation/instances.php +++ b/classes/automation/instances.php @@ -447,6 +447,7 @@ public function find_user_completion_conditions($conditions, $instancedata, $use } } + // If one or more conditions are enabled then the result of completion condition should be same as enabled. return ($enabled == $result) ? true : false; } diff --git a/classes/plugininfo/pulsecondition.php b/classes/plugininfo/pulsecondition.php index be9e0a0..0c82689 100644 --- a/classes/plugininfo/pulsecondition.php +++ b/classes/plugininfo/pulsecondition.php @@ -89,12 +89,12 @@ public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $h * @return array $subplugins. */ public function get_plugins_list() { - $actionplugins = \core_component::get_plugin_list('pulsecondition'); - return $actionplugins; + $conditionplugins = \core_component::get_plugin_list('pulsecondition'); + return $conditionplugins; } /** - * Get the list of action plugins woithj its base class. + * Get the list of condition plugins with its base class instance. */ public function get_plugins_base() { $plugins = $this->get_plugins_list(); @@ -102,18 +102,18 @@ public function get_plugins_base() { if (!empty($plugins)) { foreach ($plugins as $componentname => $pluginpath) { $instance = $this->get_plugin($componentname); - $actions[$componentname] = $instance; + $conditions[$componentname] = $instance; } } - return $actions ?? []; + return $conditions ?? []; } /** - * Get the action component actionform instance. + * Get the condtion component actionform instance. * * @param string $componentname - * @return \actionform + * @return \conditionform */ public function get_plugin($componentname) { @@ -143,13 +143,13 @@ public static function instance() { * @return stdclass */ public static function get_list() { - static $actionplugins = null; + static $conditionplugins = null; - if (!$actionplugins) { - $actionplugins = new self(); + if (!$conditionplugins) { + $conditionplugins = new self(); } - $plugins = $actionplugins->get_plugins_base(); + $plugins = $conditionplugins->get_plugins_base(); return $plugins; } } diff --git a/version.php b/version.php index 62b9467..12c7ceb 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_pulse'; -$plugin->version = 2023100704; +$plugin->version = 2023100705; $plugin->requires = 2020061500; // Requires Moodle 3.90. $plugin->release = 'v2.0'; $plugin->maturity = MATURITY_RC; From 0263ef270502ce66ecd9443c9cfba0d1ceafa630 Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Thu, 15 Feb 2024 18:25:19 +0530 Subject: [PATCH 8/9] Implemented custom mail processor to support CC/BCC --- actions/notification/classes/helper.php | 405 +++++++++++++ actions/notification/classes/manager.php | 205 +++++++ actions/notification/classes/notification.php | 4 +- actions/notification/classes/schedule.php | 10 +- .../pulseaction_notification_email.php | 547 ++++++++++++++++++ actions/notification/version.php | 2 +- 6 files changed, 1164 insertions(+), 9 deletions(-) create mode 100644 actions/notification/classes/helper.php create mode 100644 actions/notification/classes/manager.php create mode 100644 actions/notification/pulseaction_notification_email.php diff --git a/actions/notification/classes/helper.php b/actions/notification/classes/helper.php new file mode 100644 index 0000000..9c7782b --- /dev/null +++ b/actions/notification/classes/helper.php @@ -0,0 +1,405 @@ +. + +/** + * Pulse notification action helper - Contains methods to send pulse notifications to users. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace pulseaction_notification; + +use stdClass; +use core_user; +use cache; + +/** + * Pulse notification action helper to send messages to users. + */ +class helper { + + /** + * Send pulse notifications to the users. + * + * @param mixed $userto + * @param mixed $subject + * @param mixed $messageplain + * @param mixed $messagehtml + * @param mixed $pulse + * @param mixed $sender + * @param array $cc List of cc users. + * @param array $bcc List of bcc users. + * + * @return void + */ + public static function messagetouser($userto, $subject, $messageplain, $messagehtml, $pulse, $sender=true, $cc=[], $bcc=[]) { + global $CFG; + + require_once($CFG->dirroot.'/mod/pulse/lib.php'); + + $eventdata = new \core\message\message(); + $eventdata->name = 'mod_pulse'; + $eventdata->component = 'mod_pulse'; + $eventdata->courseid = $pulse->course; + $eventdata->modulename = 'pulse'; + $eventdata->userfrom = $sender ? $sender : core_user::get_support_user(); + $eventdata->userto = $userto; + $eventdata->subject = $subject; + $eventdata->fullmessage = $messageplain; + $eventdata->fullmessageformat = FORMAT_HTML; + $eventdata->fullmessagehtml = $messagehtml; + $eventdata->smallmessage = $subject; + $eventdata->customdata = ['cc' => $cc, 'bcc' => $bcc]; + if (self::message_send($eventdata)) { + pulse_mtrace( "Pulse send to the user."); + return true; + } else { + pulse_mtrace( "Failed - Pulse send to the user. -".fullname($userto), true); + return false; + } + } + + /** + * MODIFIED the core message_send method to send the message using pulse notification manager instead of core/mesage/manager. + * + * Called when a message provider wants to send a message. + * This functions checks the message recipient's message processor configuration then + * sends the message to the configured processors + * + * Required parameters of the $eventdata object: + * component string component name. must exist in message_providers + * name string message type name. must exist in message_providers + * userfrom object|int the user sending the message + * userto object|int the message recipient + * subject string the message subject + * fullmessage string the full message in a given format + * fullmessageformat int the format if the full message (FORMAT_MOODLE, FORMAT_HTML, ..) + * fullmessagehtml string the full version (the message processor will choose with one to use) + * smallmessage string the small version of the message + * + * Optional parameters of the $eventdata object: + * notification bool should the message be considered as a notification rather than a personal message + * contexturl string if this is a notification then you can specify a url to view the event. + * For example the forum post the user is being notified of. + * contexturlname string the display text for contexturl + * + * Note: processor failure will not reported as false return value in all scenarios, + * for example when it is called while a database transaction is open, + * earlier versions did not do it consistently either. + * + * @copyright 2008 Luis Rodrigues and Martin Dougiamas + * @category message + * @param \core\message\message $eventdata information about the message (component, userfrom, userto, ...) + * @return mixed the integer ID of the new message or false if there was a problem + */ + public static function message_send(\core\message\message $eventdata) { + global $CFG, $DB, $SITE; + + require_once($CFG->dirroot. '/lib/messagelib.php'); + + // New message ID to return. + $messageid = false; + + // Fetch default (site) preferences. + $defaultpreferences = get_message_output_default_preferences(); + $preferencebase = $eventdata->component.'_'.$eventdata->name; + + // If the message provider is disabled via preferences, then don't send the message. + if (!empty($defaultpreferences->{$preferencebase.'_disable'})) { + return $messageid; + } + + // By default a message is a notification. Only personal/private messages aren't notifications. + if (!isset($eventdata->notification)) { + $eventdata->notification = 1; + } + + if (!is_object($eventdata->userfrom)) { + $eventdata->userfrom = core_user::get_user($eventdata->userfrom); + } + if (!$eventdata->userfrom) { + debugging('Attempt to send msg from unknown user', DEBUG_NORMAL); + return false; + } + + // Legacy messages (FROM a single user TO a single user) must be converted into conversation messages. + // Then, these will be passed through the conversation messages code below. + if (!$eventdata->notification && !$eventdata->convid) { + // If messaging is disabled at the site level, then the 'instantmessage' provider is always disabled. + // Given this is the only 'message' type message provider, we can exit now if this is the case. + // Don't waste processing time trying to work out the other conversation member, + // If it's an individual conversation, just throw a generic debugging notice and return. + if (empty($CFG->messaging) || $eventdata->component !== 'moodle' || $eventdata->name !== 'instantmessage') { + debugging('Attempt to send msg from a provider '.$eventdata->component.'/'.$eventdata->name. + ' that is inactive or not allowed for the user id='.$eventdata->userto->id, DEBUG_NORMAL); + return false; + } + + if (!is_object($eventdata->userto)) { + $eventdata->userto = core_user::get_user($eventdata->userto); + } + if (!$eventdata->userto) { + debugging('Attempt to send msg to unknown user', DEBUG_NORMAL); + return false; + } + + // Verify all necessary data fields are present. + if (!isset($eventdata->userto->auth) or !isset($eventdata->userto->suspended) + or !isset($eventdata->userto->deleted) or !isset($eventdata->userto->emailstop)) { + + debugging('Necessary properties missing in userto object, fetching full record', DEBUG_DEVELOPER); + $eventdata->userto = core_user::get_user($eventdata->userto->id); + } + + $usertoisrealuser = (core_user::is_real_user($eventdata->userto->id) != false); + // If recipient is internal user (noreply user), and emailstop is set then don't send any msg. + if (!$usertoisrealuser && !empty($eventdata->userto->emailstop)) { + debugging('Attempt to send msg to internal (noreply) user', DEBUG_NORMAL); + return false; + } + + if ($eventdata->userfrom->id == $eventdata->userto->id) { + // It's a self conversation. + $conversation = \core_message\api::get_self_conversation($eventdata->userfrom->id); + if (empty($conversation)) { + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, + [$eventdata->userfrom->id] + ); + } + } else { + if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id, + $eventdata->userto->id])) { + // It's a private conversation between users. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $eventdata->userfrom->id, + $eventdata->userto->id + ] + ); + } + } + // We either have found a conversation, or created one. + $conversationid = !empty($conversationid) ? $conversationid : $conversation->id; + $eventdata->convid = $conversationid; + } + + // This is a message directed to a conversation, not a specific user as was the way in legacy messaging. + // The above code has adapted the legacy messages into conversation messages. + // We must call send_message_to_conversation(), which handles per-member processor iteration and triggers + // a per-conversation event. + // All eventdata for messages should now have a convid, as we fixed this above. + if (!$eventdata->notification) { + + // Only one message will be saved to the DB. + $conversationid = $eventdata->convid; + $table = 'messages'; + $tabledata = new stdClass(); + $tabledata->courseid = $eventdata->courseid; + $tabledata->useridfrom = $eventdata->userfrom->id; + $tabledata->conversationid = $conversationid; + $tabledata->subject = $eventdata->subject; + $tabledata->fullmessage = $eventdata->fullmessage; + $tabledata->fullmessageformat = $eventdata->fullmessageformat; + $tabledata->fullmessagehtml = $eventdata->fullmessagehtml; + $tabledata->smallmessage = $eventdata->smallmessage; + $tabledata->timecreated = time(); + $tabledata->customdata = $eventdata->customdata; + + // The Trusted Content system. + // Texts created or uploaded by such users will be marked as trusted and will not be cleaned before display. + if (trusttext_active()) { + // Individual conversations are always in system context. + $messagecontext = \context_system::instance(); + // We need to know the type of conversation and the contextid if it is a group conversation. + if ($conv = $DB->get_record('message_conversations', ['id' => $conversationid], 'id, type, contextid')) { + if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP && $conv->contextid) { + $messagecontext = \context::instance_by_id($conv->contextid); + } + } + $tabledata->fullmessagetrust = trusttext_trusted($messagecontext); + } else { + $tabledata->fullmessagetrust = false; + } + + if ($messageid = message_handle_phpunit_redirection($eventdata, $table, $tabledata)) { + return $messageid; + } + + // Cache messages. + if (!empty($eventdata->convid)) { + // Cache the timecreated value of the last message in this conversation. + $cache = cache::make('core', 'message_time_last_message_between_users'); + $key = \core_message\helper::get_last_message_time_created_cache_key($eventdata->convid); + $cache->set($key, $tabledata->timecreated); + } + + // Store unread message just in case we get a fatal error any time later. + $tabledata->id = $DB->insert_record($table, $tabledata); + $eventdata->savedmessageid = $tabledata->id; + + return \core\message\manager::send_message_to_conversation($eventdata, $tabledata); + } + + // Else the message is a notification. + if (!is_object($eventdata->userto)) { + $eventdata->userto = core_user::get_user($eventdata->userto); + } + if (!$eventdata->userto) { + debugging('Attempt to send msg to unknown user', DEBUG_NORMAL); + return false; + } + + // If the provider's component is disabled or the user can't receive messages from it, don't send the message. + $isproviderallowed = false; + foreach (message_get_providers_for_user($eventdata->userto->id) as $provider) { + if ($provider->component === $eventdata->component && $provider->name === $eventdata->name) { + $isproviderallowed = true; + break; + } + } + if (!$isproviderallowed) { + debugging('Attempt to send msg from a provider '.$eventdata->component.'/'.$eventdata->name. + ' that is inactive or not allowed for the user id='.$eventdata->userto->id, DEBUG_NORMAL); + return false; + } + + // Verify all necessary data fields are present. + if (!isset($eventdata->userto->auth) or !isset($eventdata->userto->suspended) + or !isset($eventdata->userto->deleted) or !isset($eventdata->userto->emailstop)) { + + debugging('Necessary properties missing in userto object, fetching full record', DEBUG_DEVELOPER); + $eventdata->userto = core_user::get_user($eventdata->userto->id); + } + + $usertoisrealuser = (core_user::is_real_user($eventdata->userto->id) != false); + // If recipient is internal user (noreply user), and emailstop is set then don't send any msg. + if (!$usertoisrealuser && !empty($eventdata->userto->emailstop)) { + debugging('Attempt to send msg to internal (noreply) user', DEBUG_NORMAL); + return false; + } + + // Check if we are creating a notification or message. + $table = 'notifications'; + + $tabledata = new stdClass(); + $tabledata->useridfrom = $eventdata->userfrom->id; + $tabledata->useridto = $eventdata->userto->id; + $tabledata->subject = $eventdata->subject; + $tabledata->fullmessage = $eventdata->fullmessage; + $tabledata->fullmessageformat = $eventdata->fullmessageformat; + $tabledata->fullmessagehtml = $eventdata->fullmessagehtml; + $tabledata->smallmessage = $eventdata->smallmessage; + $tabledata->eventtype = $eventdata->name; + $tabledata->component = $eventdata->component; + $tabledata->timecreated = time(); + $tabledata->customdata = $eventdata->customdata; + if (!empty($eventdata->contexturl)) { + $tabledata->contexturl = (string)$eventdata->contexturl; + } else { + $tabledata->contexturl = null; + } + + if (!empty($eventdata->contexturlname)) { + $tabledata->contexturlname = (string)$eventdata->contexturlname; + } else { + $tabledata->contexturlname = null; + } + + if ($messageid = message_handle_phpunit_redirection($eventdata, $table, $tabledata)) { + return $messageid; + } + + // Fetch enabled processors. + $processors = get_message_processors(true); + + // Preset variables. + $processorlist = array(); + // Fill in the array of processors to be used based on default and user preferences. + foreach ($processors as $processor) { + // Skip adding processors for internal user, if processor doesn't support sending message to internal user. + if (!$usertoisrealuser && !$processor->object->can_send_to_any_users()) { + continue; + } + + // First find out permissions. + $defaultlockedpreference = $processor->name . '_provider_' . $preferencebase . '_locked'; + $locked = false; + if (isset($defaultpreferences->{$defaultlockedpreference})) { + $locked = $defaultpreferences->{$defaultlockedpreference}; + } else { + // MDL-25114 They supplied an $eventdata->component $eventdata->name combination which doesn't. + // exist in the message_provider table (thus there is no default settings for them). + $preferrormsg = "Could not load preference $defaultlockedpreference. Make sure the component and name you supplied + to message_send() are valid."; + throw new \coding_exception($preferrormsg); + } + + $preferencename = 'message_provider_'.$preferencebase.'_enabled'; + $forced = false; + if ($locked && isset($defaultpreferences->{$preferencename})) { + $userpreference = $defaultpreferences->{$preferencename}; + $forced = in_array($processor->name, explode(',', $userpreference)); + } + + // Find out if user has configured this output. + // Some processors cannot function without settings from the user. + $userisconfigured = $processor->object->is_user_configured($eventdata->userto); + + // DEBUG: notify if we are forcing unconfigured output. + if ($forced && !$userisconfigured) { + debugging( + 'Attempt to force message delivery to user who has "'.$processor->name.'" output unconfigured', DEBUG_NORMAL); + } + + // Populate the list of processors we will be using. + if ($forced && $userisconfigured) { + // An admin is forcing users to use this message processor. Use this processor unconditionally. + $processorlist[] = $processor->name; + } else if (!$forced && !$locked && $userisconfigured && !$eventdata->userto->emailstop) { + // User has not disabled notifications. + // See if user set any notification preferences, otherwise use site default ones. + if ($userpreference = get_user_preferences($preferencename, null, $eventdata->userto)) { + if (in_array($processor->name, explode(',', $userpreference))) { + $processorlist[] = $processor->name; + } + } else if (isset($defaultpreferences->{$preferencename})) { + if (in_array($processor->name, explode(',', $defaultpreferences->{$preferencename}))) { + $processorlist[] = $processor->name; + } + } + } + } + + // Store unread message just in case we get a fatal error any time later. + $tabledata->id = $DB->insert_record($table, $tabledata); + $eventdata->savedmessageid = $tabledata->id; + + // Let the manager do the sending or buffering when db transaction in progress. + try { + // PULSE MODIFY - Send the message using pulse notification manager instead of core/mesage/manager. + return \pulseaction_notification\manager::send_message($eventdata, $tabledata, $processorlist); + } catch (\moodle_exception $exception) { + return false; + } + } + + + +} diff --git a/actions/notification/classes/manager.php b/actions/notification/classes/manager.php new file mode 100644 index 0000000..dacba48 --- /dev/null +++ b/actions/notification/classes/manager.php @@ -0,0 +1,205 @@ +. + +/** + * Notification pulse action - Message manager. + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace pulseaction_notification; + +use core\message\message; + +/** + * Helps to send messages from available message processesors, extends the moodle core\message\manager method. + */ +class manager extends \core\message\manager { + + /** + * Do the message sending - The method to use the custom call_processor method. + * + * NOTE: to be used from message_send() only. + * + * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @author Petr Skoda + * + * @param \core\message\message $eventdata fully prepared event data for processors + * @param \stdClass $savemessage the message saved in 'message' table + * @param array $processorlist list of processors for target user + * @return int $messageid the id from 'messages' (false is not returned) + */ + public static function send_message(message $eventdata, \stdClass $savemessage, array $processorlist) { + global $CFG; + + require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file. + + if (empty($processorlist)) { + // Trigger event for sending a message or notification - we need to do this before marking as read! + self::trigger_message_events($eventdata, $savemessage); + + if ($eventdata->notification) { + // If they have deselected all processors and it's a notification mark it read. The user doesn't want to be + // bothered. + $savemessage->timeread = null; + \core_message\api::mark_notification_as_read($savemessage); + } else if (empty($CFG->messaging)) { + // If it's a message and messaging is disabled mark it read. + \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage); + } + + return $savemessage->id; + } + + // Let the manager do the sending or buffering when db transaction in progress. + return self::send_message_to_processors($eventdata, $savemessage, $processorlist); + } + + /** + * Send message to message processors - Inherit the method to use the custom call_processor method. + * + * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @author Petr Skoda + * + * @param \stdClass|\core\message\message $eventdata + * @param \stdClass $savemessage + * @param array $processorlist + * @throws \moodle_exception + * @return int $messageid + */ + protected static function send_message_to_processors($eventdata, \stdClass $savemessage, array + $processorlist) { + global $CFG, $DB; + + // We cannot communicate with external systems in DB transactions, + // buffer the messages if necessary. + if ($DB->is_transaction_started()) { + // We need to clone all objects so that devs may not modify it from outside later. + $eventdata = clone($eventdata); + $eventdata->userto = clone($eventdata->userto); + $eventdata->userfrom = clone($eventdata->userfrom); + + // Conserve some memory the same was as $USER setup does. + unset($eventdata->userto->description); + unset($eventdata->userfrom->description); + + self::$buffer[] = array($eventdata, $savemessage, $processorlist); + return $savemessage->id; + } + + // Send the message to processors. + if (!self::call_processors($eventdata, $processorlist)) { + throw new \moodle_exception("Message was not sent."); + } + + // Trigger event for sending a message or notification - we need to do this before marking as read! + self::trigger_message_events($eventdata, $savemessage); + + if (!$eventdata->notification && empty($CFG->messaging)) { + // If it's a message and messaging is disabled mark it read. + \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage); + } + + return $savemessage->id; + } + + /** + * For each processor, call it's send_message() method + * - This method is modified version of core\message\manager call_processors. + * - Modifed to use our custom email message procesessor instead of default message_output_email method. + * - By updating the email processor pulse includes the CC and Bcc emails. + * + * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} + * @author Petr Skoda + * + * @param message $eventdata the message object. + * @param array $processorlist the list of processors for a single user. + * @return bool false if error calling message processor + */ + protected static function call_processors(message $eventdata, array $processorlist) { + // Allow plugins to change the message/notification data before sending it. + $pluginsfunction = get_plugins_with_function('pre_processor_message_send'); + $sendmsgsuccessful = true; + foreach ($processorlist as $procname) { + // Let new messaging class add custom content based on the processor. + $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata; + + if ($pluginsfunction) { + foreach ($pluginsfunction as $plugintype => $plugins) { + foreach ($plugins as $pluginfunction) { + $pluginfunction($procname, $proceventdata); + } + } + } + + $stdproc = new \stdClass(); + $stdproc->name = $procname; + + // Call the pulse email process instead of message_email_output. + $processor = ($procname == 'email') + ? self::get_processed_processor_object($stdproc) : \core_message\api::get_processed_processor_object($stdproc); + if (!$processor->object->send_message($proceventdata)) { + debugging('Error calling message processor ' . $procname); + $sendmsgsuccessful = false; + } + } + return $sendmsgsuccessful; + } + + /** + * Modified version of \core_message\api::get_processed_processor_object. + * - Fetch the pulseaction_notification_email processor. This helps to use pulse custom email processor. + * + * Given a processor object, loads information about it's settings and configurations. + * This is not a public api, instead use {@see \core_message\api::get_message_processor()} + * or {@see \get_message_processors()} + * + * @copyright 2016 Mark Nelson + * + * @param \stdClass $processor processor object + * @return \stdClass processed processor object + * @since Moodle 3.2 + */ + public static function get_processed_processor_object(\stdClass $processor) { + global $CFG; + + $processorfile = $CFG->dirroot. '/mod/pulse/actions/notification/pulseaction_notification_email.php'; + if (is_readable($processorfile)) { + include_once($processorfile); + $processclass = 'pulseaction_notification_email'; + if (class_exists($processclass)) { + $pclass = new $processclass(); + $processor->object = $pclass; + $processor->configured = 0; + if ($pclass->is_system_configured()) { + $processor->configured = 1; + } + $processor->hassettings = 0; + if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) { + $processor->hassettings = 1; + } + $processor->available = 1; + } else { + throw new \moodle_exception('errorcallingprocessor', 'message'); + } + } else { + $processor->available = 0; + } + return $processor; + } +} diff --git a/actions/notification/classes/notification.php b/actions/notification/classes/notification.php index 11b0599..98eb269 100644 --- a/actions/notification/classes/notification.php +++ b/actions/notification/classes/notification.php @@ -922,8 +922,8 @@ public function generate_notification_details($moddata, $user, $context, $notifi $result = [ 'recepient' => (object) $user, - 'cc' => implode(',', array_column($ccusers, 'email')), - 'bcc' => implode(',', array_column($bccusers, 'email')), + 'cc' => array_map(fn($user) => [$user->email, fullname($user)], $ccusers), + 'bcc' => array_map(fn($user) => [$user->email, fullname($user)], $bccusers), 'subject' => format_string($this->notificationdata->subject), 'content' => $this->build_notification_content($moddata, $context, $notificationoverrides), ]; diff --git a/actions/notification/classes/schedule.php b/actions/notification/classes/schedule.php index 60a2da6..b4ebbc7 100644 --- a/actions/notification/classes/schedule.php +++ b/actions/notification/classes/schedule.php @@ -168,10 +168,8 @@ public function send_scheduled_notification($userid=null) { $sender = $this->find_sender_user(); // Add bcc and CC to sender user custom headers. - $sender->customheaders = [ - "Bcc: $detail->bcc\r\n", - "Cc: $detail->cc\r\n", - ]; + $cc = $detail->cc ?? []; + $bcc = $detail->bcc ?? []; // Prepare the module data. based on dynamic content and includ the session data. $mod = $this->prepare_moduledata_placeholders($modules, $cmdata); @@ -195,8 +193,8 @@ public function send_scheduled_notification($userid=null) { $pulse = (object) ['course' => $this->course->id]; // TODO: NOTE using notification API takes 16 queries. Direct email_to_user method will take totally 9 queries. // Send the notification to user. - $messagesend = \mod_pulse\helper::messagetouser( - $detail->recepient, $subject, $messageplain, $messagehtml, $pulse, $sender + $messagesend = \pulseaction_notification\helper::messagetouser( + $detail->recepient, $subject, $messageplain, $messagehtml, $pulse, $sender, $cc, $bcc ); if ($messagesend) { diff --git a/actions/notification/pulseaction_notification_email.php b/actions/notification/pulseaction_notification_email.php new file mode 100644 index 0000000..aed6de8 --- /dev/null +++ b/actions/notification/pulseaction_notification_email.php @@ -0,0 +1,547 @@ +. + +/** + * Contains the definiton of the email message processors (sends messages to users via email) + * + * @package pulseaction_notification + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/message/output/email/message_output_email.php'); + +/** + * The email message processor for pulse notification actions. Extends the core message_output_email processor. + */ +class pulseaction_notification_email extends message_output_email { + + /** + * Processes the message (sends by email). + * - Modified the core message_output_email plugin method to send CC and BCC mails. + * + * @copyright 2008 Luis Rodrigues and Martin Dougiamas + * @param object $eventdata the event data submitted by the message sender plus $eventdata->savedmessageid + */ + public function send_message($eventdata) { + global $CFG, $DB; + + // Skip any messaging suspended and deleted users. + if ($eventdata->userto->auth === 'nologin' or $eventdata->userto->suspended or $eventdata->userto->deleted) { + return true; + } + + // The user the email is going to. + $recipient = null; + + // Check if the recipient has a different email address specified in their messaging preferences Vs their user profile. + $emailmessagingpreference = get_user_preferences('message_processor_email_email', null, $eventdata->userto); + $emailmessagingpreference = clean_param($emailmessagingpreference, PARAM_EMAIL); + + // If the recipient has set an email address in their preferences use that instead of the one in their profile, + // But only if overriding the notification email address is allowed. + if (!empty($emailmessagingpreference) && !empty($CFG->messagingallowemailoverride)) { + // Clone to avoid altering the actual user object. + $recipient = clone($eventdata->userto); + $recipient->email = $emailmessagingpreference; + } else { + $recipient = $eventdata->userto; + } + + // Check if we have attachments to send. + $attachment = ''; + $attachname = ''; + if (!empty($CFG->allowattachments) && !empty($eventdata->attachment)) { + if (empty($eventdata->attachname)) { + // Attachment needs a file name. + debugging('Attachments should have a file name. No attachments have been sent.', DEBUG_DEVELOPER); + } else if (!($eventdata->attachment instanceof stored_file)) { + // Attachment should be of a type stored_file. + debugging('Attachments should be of type stored_file. No attachments have been sent.', DEBUG_DEVELOPER); + } else { + // Copy attachment file to a temporary directory and get the file path. + $attachment = $eventdata->attachment->copy_content_to_temp(); + + // Get attachment file name. + $attachname = clean_filename($eventdata->attachname); + } + } + + // Configure mail replies - this is used for incoming mail replies. + $replyto = ''; + $replytoname = ''; + if (isset($eventdata->replyto)) { + $replyto = $eventdata->replyto; + if (isset($eventdata->replytoname)) { + $replytoname = $eventdata->replytoname; + } + } + + // Pulse - section to add bcc and cc users. + $ccaddr = []; + $bccaddr = []; + $customdata = json_decode($eventdata->customdata); + if (isset($eventdata->customdata) && isset($customdata->cc)) { + $ccaddr = $customdata->cc; + $bccaddr = $customdata->bcc; + } + + // We email messages from private conversations straight away, but for group we add them to a table to be sent later. + $emailuser = true; + if (!$eventdata->notification) { + if ($eventdata->conversationtype == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) { + $emailuser = false; + } + } + + if ($emailuser) { + $result = $this->email_to_user($recipient, $eventdata->userfrom, $eventdata->subject, $eventdata->fullmessage, + $eventdata->fullmessagehtml, $attachment, $attachname, true, $replyto, $replytoname, 79, $ccaddr, $bccaddr); + } else { + $messagetosend = new stdClass(); + $messagetosend->useridfrom = $eventdata->userfrom->id; + $messagetosend->useridto = $recipient->id; + $messagetosend->conversationid = $eventdata->convid; + $messagetosend->messageid = $eventdata->savedmessageid; + $result = $DB->insert_record('message_email_messages', $messagetosend, false); + } + + // Remove an attachment file if any. + if (!empty($attachment) && file_exists($attachment)) { + unlink($attachment); + } + + return $result; + } + + /** + * Send an email to a specified user - Modified the core moodlelib function to send CC and BCC mails. + * + * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com + * + * @param stdClass $user A {@see $USER} object + * @param stdClass $from A {@see $USER} object + * @param string $subject plain text subject line of the email + * @param string $messagetext plain text version of the message + * @param string $messagehtml complete html version of the message (optional) + * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of + * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir + * @param string $attachname the name of the file (extension indicates MIME) + * @param bool $usetrueaddress determines whether $from email address should + * be sent out. Will be overruled by user profile setting for maildisplay + * @param string $replyto Email address to reply to + * @param string $replytoname Name of reply to recipient + * @param int $wordwrapwidth custom word wrap width, default 79 + * @param array $cc Cc mails list. + * @param array $bcc Bcc mails list. + * + * @return bool Returns true if mail was sent OK and false if there was an error. + */ + public function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '', + $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79, $cc=[], $bcc=[]) { + + global $CFG, $PAGE, $SITE; + + if (empty($user) or empty($user->id)) { + debugging('Can not send email to null user', DEBUG_DEVELOPER); + return false; + } + + if (empty($user->email)) { + debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER); + return false; + } + + if (!empty($user->deleted)) { + debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER); + return false; + } + + if (defined('BEHAT_SITE_RUNNING')) { + // Fake email sending in behat. + return true; + } + + if (!empty($CFG->noemailever)) { + // Hidden setting for development sites, set in config.php if needed. + debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL); + return true; + } + + if (email_should_be_diverted($user->email)) { + $subject = "[DIVERTED {$user->email}] $subject"; + $user = clone($user); + $user->email = $CFG->divertallemailsto; + } + + // Skip mail to suspended users. + if ((isset($user->auth) && $user->auth == 'nologin') or (isset($user->suspended) && $user->suspended)) { + return true; + } + + if (!validate_email($user->email)) { + // We can not send emails to invalid addresses - it might create security issue or confuse the mailer. + debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending."); + return false; + } + + if (over_bounce_threshold($user)) { + debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending."); + return false; + } + + // TLD .invalid is specifically reserved for invalid domain names. + // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}. + if (substr($user->email, -8) == '.invalid') { + debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending."); + return true; // This is not an error. + } + + // If the user is a remote mnet user, parse the email text for URL to the + // wwwroot and modify the url to direct the user's browser to login at their + // home site (identity provider - idp) before hitting the link itself. + if (is_mnet_remote_user($user)) { + require_once($CFG->dirroot.'/mnet/lib.php'); + + $jumpurl = mnet_get_idp_jump_url($user); + $callback = partial('mnet_sso_apply_indirection', $jumpurl); + + $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%", + $callback, + $messagetext); + $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%", + $callback, + $messagehtml); + } + $mail = get_mailer(); + + if (!empty($mail->SMTPDebug)) { + echo '
' . "\n";
+        }
+
+        $temprecipients = array();
+        $tempreplyto = array();
+
+        // Make sure that we fall back onto some reasonable no-reply address.
+        $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
+        $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
+
+        if (!validate_email($noreplyaddress)) {
+            debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
+            $noreplyaddress = $noreplyaddressdefault;
+        }
+
+        // Make up an email address for handling bounces.
+        if (!empty($CFG->handlebounces)) {
+            $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
+            $mail->Sender = generate_email_processing_address(0, $modargs);
+        } else {
+            $mail->Sender = $noreplyaddress;
+        }
+
+        // Make sure that the explicit replyto is valid, fall back to the implicit one.
+        if (!empty($replyto) && !validate_email($replyto)) {
+            debugging('email_to_user: Invalid replyto-email '.s($replyto));
+            $replyto = $noreplyaddress;
+        }
+
+        if (is_string($from)) { // So we can pass whatever we want if there is need.
+            $mail->From     = $noreplyaddress;
+            $mail->FromName = $from;
+            // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
+            // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
+            // in a course with the sender.
+        } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
+            if (!validate_email($from->email)) {
+                debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
+                // Better not to use $noreplyaddress in this case.
+                return false;
+            }
+            $mail->From = $from->email;
+            $fromdetails = new stdClass();
+            $fromdetails->name = fullname($from);
+            $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
+            $fromdetails->siteshortname = format_string($SITE->shortname);
+            $fromstring = $fromdetails->name;
+            if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
+                $fromstring = get_string('emailvia', 'core', $fromdetails);
+            }
+            $mail->FromName = $fromstring;
+            if (empty($replyto)) {
+                $tempreplyto[] = array($from->email, fullname($from));
+            }
+        } else {
+            $mail->From = $noreplyaddress;
+            $fromdetails = new stdClass();
+            $fromdetails->name = fullname($from);
+            $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
+            $fromdetails->siteshortname = format_string($SITE->shortname);
+            $fromstring = $fromdetails->name;
+            if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
+                $fromstring = get_string('emailvia', 'core', $fromdetails);
+            }
+            $mail->FromName = $fromstring;
+            if (empty($replyto)) {
+                $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
+            }
+        }
+
+        if (!empty($replyto)) {
+            $tempreplyto[] = array($replyto, $replytoname);
+        }
+
+        $temprecipients[] = array($user->email, fullname($user));
+
+        // Set word wrap.
+        $mail->WordWrap = $wordwrapwidth;
+
+        if (!empty($from->customheaders)) {
+            // Add custom headers.
+            if (is_array($from->customheaders)) {
+                foreach ($from->customheaders as $customheader) {
+                    $mail->addCustomHeader($customheader);
+                }
+            } else {
+                $mail->addCustomHeader($from->customheaders);
+            }
+        }
+
+        // If the X-PHP-Originating-Script email header is on then also add an additional
+        // header with details of where exactly in moodle the email was triggered from,
+        // either a call to message_send() or to email_to_user().
+        if (ini_get('mail.add_x_header')) {
+
+            $stack = debug_backtrace(false);
+            $origin = $stack[0];
+
+            foreach ($stack as $depth => $call) {
+                if ($call['function'] == 'message_send') {
+                    $origin = $call;
+                }
+            }
+
+            $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
+                . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
+            $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
+        }
+
+        if (!empty($CFG->emailheaders)) {
+            $headers = array_map('trim', explode("\n", $CFG->emailheaders));
+            foreach ($headers as $header) {
+                if (!empty($header)) {
+                    $mail->addCustomHeader($header);
+                }
+            }
+        }
+
+        if (!empty($from->priority)) {
+            $mail->Priority = $from->priority;
+        }
+
+        $renderer = $PAGE->get_renderer('core');
+        $context = array(
+            'sitefullname' => $SITE->fullname,
+            'siteshortname' => $SITE->shortname,
+            'sitewwwroot' => $CFG->wwwroot,
+            'subject' => $subject,
+            'prefix' => $CFG->emailsubjectprefix,
+            'to' => $user->email,
+            'toname' => fullname($user),
+            'from' => $mail->From,
+            'fromname' => $mail->FromName,
+        );
+        if (!empty($tempreplyto[0])) {
+            $context['replyto'] = $tempreplyto[0][0];
+            $context['replytoname'] = $tempreplyto[0][1];
+        }
+        if ($user->id > 0) {
+            $context['touserid'] = $user->id;
+            $context['tousername'] = $user->username;
+        }
+
+        if (!empty($user->mailformat) && $user->mailformat == 1) {
+            // Only process html templates if the user preferences allow html email.
+
+            if (!$messagehtml) {
+                // If no html has been given, BUT there is an html wrapping template then
+                // auto convert the text to html and then wrap it.
+                $messagehtml = trim(text_to_html($messagetext));
+            }
+            $context['body'] = $messagehtml;
+            $messagehtml = $renderer->render_from_template('core/email_html', $context);
+        }
+
+        $context['body'] = html_to_text(nl2br($messagetext));
+        $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
+        $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
+        $messagetext = $renderer->render_from_template('core/email_text', $context);
+
+        // Autogenerate a MessageID if it's missing.
+        if (empty($mail->MessageID)) {
+            $mail->MessageID = generate_email_messageid();
+        }
+
+        if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
+            // Don't ever send HTML to users who don't want it.
+            $mail->isHTML(true);
+            $mail->Encoding = 'quoted-printable';
+            $mail->Body    = $messagehtml;
+            $mail->AltBody = "\n$messagetext\n";
+        } else {
+            $mail->IsHTML(false);
+            $mail->Body = "\n$messagetext\n";
+        }
+
+        if ($attachment && $attachname) {
+            if (preg_match( "~\\.\\.~" , $attachment )) {
+                // Security check for ".." in dir path.
+                $supportuser = core_user::get_support_user();
+                $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
+                $mail->addStringAttachment(
+                    'Error in attachment.  User attempted to attach a filename with a unsafe name.',
+                    'error.txt', '8bit', 'text/plain');
+            } else {
+                require_once($CFG->libdir.'/filelib.php');
+                $mimetype = mimeinfo('type', $attachname);
+
+                // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
+                // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
+                $attachpath = str_replace('\\', '/', realpath($attachment));
+
+                // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
+                $allowedpaths = array_map(function(string $path): string {
+                    return str_replace('\\', '/', realpath($path));
+                }, [
+                    $CFG->cachedir,
+                    $CFG->dataroot,
+                    $CFG->dirroot,
+                    $CFG->localcachedir,
+                    $CFG->tempdir,
+                    $CFG->localrequestdir,
+                ]);
+
+                // Set addpath to true.
+                $addpath = true;
+
+                // Check if attachment includes one of the allowed paths.
+                foreach (array_filter($allowedpaths) as $allowedpath) {
+                    // Set addpath to false if the attachment includes one of the allowed paths.
+                    if (strpos($attachpath, $allowedpath) === 0) {
+                        $addpath = false;
+                        break;
+                    }
+                }
+
+                // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
+                // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
+                if ($addpath == true) {
+                    $attachment = $CFG->dataroot . '/' . $attachment;
+                }
+
+                $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
+            }
+        }
+
+        // Check if the email should be sent in an other charset then the default UTF-8.
+        if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
+
+            // Use the defined site mail charset or eventually the one preferred by the recipient.
+            $charset = $CFG->sitemailcharset;
+            if (!empty($CFG->allowusermailcharset)) {
+                if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
+                    $charset = $useremailcharset;
+                }
+            }
+
+            // Convert all the necessary strings if the charset is supported.
+            $charsets = get_list_of_charsets();
+            unset($charsets['UTF-8']);
+            if (in_array($charset, $charsets)) {
+                $mail->CharSet  = $charset;
+                $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
+                $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
+                $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
+                $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
+
+                foreach ($temprecipients as $key => $values) {
+                    $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
+                }
+                foreach ($tempreplyto as $key => $values) {
+                    $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
+                }
+            }
+        }
+
+        foreach ($temprecipients as $values) {
+            $mail->addAddress($values[0], $values[1]);
+        }
+        foreach ($tempreplyto as $values) {
+            $mail->addReplyTo($values[0], $values[1]);
+        }
+
+        // Custom method to add cc and bcc.
+        foreach ($cc as $values) {
+            $mail->addCC($values[0], $values[1]);
+        }
+        foreach ($bcc as $values) {
+            $mail->addBCC($values[0], $values[1]);
+        }
+
+        if (!empty($CFG->emaildkimselector)) {
+            $domain = substr(strrchr($mail->From, "@"), 1);
+            $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
+            if (file_exists($pempath)) {
+                $mail->DKIM_domain      = $domain;
+                $mail->DKIM_private     = $pempath;
+                $mail->DKIM_selector    = $CFG->emaildkimselector;
+                $mail->DKIM_identity    = $mail->From;
+            } else {
+                debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
+            }
+        }
+
+        if ($mail->send()) {
+            set_send_count($user);
+            if (!empty($mail->SMTPDebug)) {
+                echo '
'; + } + return true; + } else { + // Trigger event for failing to send email. + $event = \core\event\email_failed::create(array( + 'context' => context_system::instance(), + 'userid' => $from->id, + 'relateduserid' => $user->id, + 'other' => array( + 'subject' => $subject, + 'message' => $messagetext, + 'errorinfo' => $mail->ErrorInfo + ) + )); + $event->trigger(); + if (CLI_SCRIPT) { + mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo); + } + if (!empty($mail->SMTPDebug)) { + echo ''; + } + return false; + } + } + +} diff --git a/actions/notification/version.php b/actions/notification/version.php index 2447d84..39bf176 100644 --- a/actions/notification/version.php +++ b/actions/notification/version.php @@ -25,4 +25,4 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = "pulseaction_notification"; -$plugin->version = 2023080409; +$plugin->version = 2023080410; From b868f081eb488ead82b6a8f019338fe6a170a2e4 Mon Sep 17 00:00:00 2001 From: Stefan-Alexander Scholz Date: Tue, 20 Feb 2024 20:08:12 +0100 Subject: [PATCH 9/9] Update version.php --- version.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.php b/version.php index 12c7ceb..7fa3d67 100644 --- a/version.php +++ b/version.php @@ -26,7 +26,7 @@ $plugin->component = 'mod_pulse'; $plugin->version = 2023100705; -$plugin->requires = 2020061500; // Requires Moodle 3.90. +$plugin->requires = 2022112800; // Requires Moodle 4.1 $plugin->release = 'v2.0'; -$plugin->maturity = MATURITY_RC; -$plugin->supported = [39, 402]; +$plugin->maturity = MATURIY_STABLE; +$plugin->supported = [401, 402];