Skip to content

Commit

Permalink
Settings to allow booking rules to send ics attachments (#801)
Browse files Browse the repository at this point in the history
* Settings to allow booking rules to send ics attachments

Setting to toggle sending an ics attachment to booking_rules and a separate toggle to denoted if the attachments should an update or create ics.

Actual attachment is handled in the message controller. This should probably be moved to the method somewhere but I wasn't sure where.

* fix issues raised by CI

* We've noticed that ics files with html or new lines in them don't render correctly in Googlemail/ calendar, so I've added X-ALT-DESC to the ics.
I've also done a quick and dirty to render the urls as links.
  • Loading branch information
danbuntu authored and ibernhardf committed Feb 11, 2025
1 parent 2405467 commit 9e8639d
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 4 deletions.
25 changes: 25 additions & 0 deletions classes/booking_rules/actions/send_mail.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ class send_mail implements booking_rule_action {
/** @var int $ruleid */
public $ruleid = null;

/** @var string $sendical */
public $sendical = null;

/** @var string $sendicalcreateorcancel */
public $sendicalcreateorcancel = null;

/** @var string $subject */
public $subject = null;

Expand All @@ -68,6 +74,8 @@ public function set_actiondata_from_json(string $json) {
$jsonobject = json_decode($json);
$actiondata = $jsonobject->actiondata;

$this->sendical = $actiondata->sendical ?? '';
$this->sendicalcreateorcancel = $actiondata->sendicalcreateorcancel ?? '';
$this->subject = $actiondata->subject;
$this->template = $actiondata->template;
}
Expand All @@ -81,6 +89,17 @@ public function set_actiondata_from_json(string $json) {
*/
public function add_action_to_mform(MoodleQuickForm &$mform, array &$repeateloptions) {

// Select to send ical.
$mform->addElement('selectyesno', 'action_send_mail_sendical', get_string('sendical', 'mod_booking'));
$mform->addRule('action_send_mail_sendical', null, 'required', null, 'client');
$mform->setType('action_send_mail_sendical', PARAM_INT);

// TODO This should probably only show if the above dropdown is set to yes.
$options = ['create' => get_string('createical', 'mod_booking'), 'cancel' => get_string('cancelical', 'mod_booking')];
$mform->addElement('select', 'action_send_mail_sendicalcreateorcancel',
get_string('sendicalcreateorcancel', 'mod_booking'), $options);
$mform->setType('action_send_mail_sendicalcreateorcancel', PARAM_RAW);

// Mail subject.
$mform->addElement('text', 'action_send_mail_subject', get_string('messagesubject', 'mod_booking'),
['size' => '66']);
Expand Down Expand Up @@ -130,6 +149,8 @@ public function save_action(stdClass &$data) {
$jsonobject->name = $data->name ?? $this->actionname;
$jsonobject->actionname = $this->actionname;
$jsonobject->actiondata = new stdClass();
$jsonobject->actiondata->sendical = $data->action_send_mail_sendical;
$jsonobject->actiondata->sendicalcreateorcancel = $data->action_send_mail_sendicalcreateorcancel;
$jsonobject->actiondata->subject = $data->action_send_mail_subject;
$jsonobject->actiondata->template = $data->action_send_mail_template['text'];
$jsonobject->actiondata->templateformat = $data->action_send_mail_template['format'];
Expand All @@ -147,6 +168,8 @@ public function set_defaults(stdClass &$data, stdClass $record) {
$jsonobject = json_decode($record->rulejson);
$actiondata = $jsonobject->actiondata;

$data->action_send_mail_sendical = $actiondata->sendical;
$data->action_send_mail_sendicalcreateorcancel = $actiondata->sendicalcreateorcancel;
$data->action_send_mail_subject = $actiondata->subject;
$data->action_send_mail_template = [];
$data->action_send_mail_template['text'] = $actiondata->template;
Expand All @@ -172,6 +195,8 @@ public function execute(stdClass $record) {
'userid' => $record->userid,
'optionid' => $record->optionid,
'cmid' => $record->cmid,
'sendical' => $this->sendical,
'sendicalcreateorcancel' => $this->sendicalcreateorcancel,
'customsubject' => $this->subject,
'custommessage' => $this->template,
'installmentnr' => $record->payment_id ?? 0,
Expand Down
23 changes: 22 additions & 1 deletion classes/ical.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,29 @@ protected function add_vevent($uid, $dtstart, $dtend, $time = false) {
}

// Make sure we have not tags in full description.
$fulldescription = rtrim(strip_tags(preg_replace( "/<br>|<\/p>/", "\n", $fulldescription)));
$fulldescriptionhtml = $fulldescription;
// Remove CR and CRLF from description as the description must be on one line.
$fulldescriptionhtml = str_replace (array("\r\n", "\n", "\r"), ' ', $fulldescriptionhtml);

// Check for a url and render it as a nice link.
// Regular Expression Pattern for a basic URL.
$pattern = '/\b(?:https?:\/\/)[a-zA-Z0-9\.\-]+(?:\.[a-zA-Z]{2,})(?:\/\S*)?/';
// Array to hold the matched URLs.
$matches = [];
// Perform the pattern match.
preg_match_all($pattern, $fulldescriptionhtml, $matches);

foreach ($matches[0] as $url) {
$fulldescriptionhtml = str_replace($url, '<a href="' . $url . '">Link</a>', $fulldescriptionhtml);
}

$fulldescription = rtrim(strip_tags(preg_replace("/<br>|<\/p>/", "\n", $fulldescription)));
$fulldescription = str_replace("\n", "\\n", $fulldescription );

// Remove CR and CRLF from description as the description must be on one line to work with ical.
$fulldescription = str_replace (array("\r\n", "\n", "\r"), ' ', $fulldescription);


// 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;
Expand All @@ -353,6 +373,7 @@ protected function add_vevent($uid, $dtstart, $dtend, $time = false) {
"BEGIN:VEVENT",
"CLASS:PUBLIC",
"DESCRIPTION:{$fulldescription}",
"X-ALT-DESC;FMTTYPE=text/html:{$fulldescriptionhtml}",
"DTEND:{$dtend}",
"DTSTAMP:{$this->dtstamp}",
"DTSTART:{$dtstart}",
Expand Down
83 changes: 80 additions & 3 deletions classes/message_controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ class message_controller {
/** @var float $price price given in installment */
private $price;

/** @var array $rulesettings duedate of installment. */
private $rulesettings;

/** @var array $ruleid the id of the running rule. */
private $ruleid;

/**
* Constructor
*
Expand All @@ -138,6 +144,7 @@ class message_controller {
* @param int $duedate UNIX timestamp for duedate of installment
* @param float $price price of installment
* @param string $rulejson event data
* @param ?int $ruleid the id of the running rule
*/
public function __construct(
int $msgcontrparam,
Expand All @@ -153,10 +160,23 @@ public function __construct(
int $installmentnr = 0,
int $duedate = 0,
float $price = 0.0,
string $rulejson = ''
string $rulejson = '',
?int $ruleid = null
) {

global $USER, $PAGE, $SESSION;
global $USER, $PAGE, $DB;

if (!is_null($ruleid)) {
// For some reason $this->rulejson doesn't get passed to the controller.
// So instead we use the ruleid that we have added to this class.
// Get the rulesjson and convert into an array for later
// There is probably an exisiting method for this, but I couldn't find it.
$this->rulesettings = $DB->get_record('booking_rules', ['id' => $ruleid], 'rulejson');
if ( $this->rulesettings) {
$this->rulesettings = json_decode($this->rulesettings->rulejson);
$this->ruleid = $ruleid;
}
}

$user = singleton_service::get_instance_of_user($userid);
$originallanguage = force_current_language($user->lang);
Expand Down Expand Up @@ -457,9 +477,63 @@ public function send_or_queue(): bool {

} else {

// If the rule has sendical set then we get the ical attachment.
// Create it in file storage and put it in the message object.
if ($this->rulesettings->actiondata->sendical) {

$update = false;
if ($this->rulesettings->actiondata->sendicalcreateorcancel == 'cancel') {
$update = true;
}

// Pass the update param - false will create a remove calendar invite.
// TODO The system still fires an unsubscribe message - I believe this is a hangover of the old non rules booking system.
list($attachments, $attachname) = $this->get_attachments($update);

if (!empty($attachments)) {
// TODO this should probably be a method in the ical class.
// Left here to limit to number of changed files.
// Store the file correctly in order to be able to attach it.
$fs = get_file_storage();
$context = context_system::instance(); // Use a suitable context, such as course or module context.
$tempfilepath = $attachments['booking.ics'];

// Check if the file exists in the temp path.
if (file_exists($tempfilepath)) {

// Prepare file record in Moodle storage.
$filerecord = [
'contextid' => $context->id,
'component' => 'mod_booking', // Change to your component.
'filearea' => 'message_attachments', // A custom file area for attachments.
'itemid' => 0, // Item ID (0 for general use or unique identifier for the message).
'filepath' => '/', // Always use '/' as the root directory.
'filename' => $attachname,
'userid' => $this->messagedata->userto->id,
];

// Create or retrieve the file in Moodle's file storage.
$storedfile = $fs->create_file_from_pathname($filerecord, $tempfilepath);

// Set the file as an attachment.
$this->messagedata->attachment = $storedfile;
$this->messagedata->attachname = $attachname;
} else {
// TODO There is possibly a better way to handle this error nicely - or remove the check entirely.
throw new \moodle_exception('Attachment file not found.');
}

}
}

// In all other cases, use message_send.
if (message_send($this->messagedata)) {

if ($this->rulesettings->actiondata->sendical) {
// Tidy up the now not needed file.
$storedfile->delete();
}

// Use an event to log that a message has been sent.
$event = \mod_booking\event\message_sent::create([
'context' => context_system::instance(),
Expand All @@ -471,6 +545,9 @@ public function send_or_queue(): bool {
'subject' => $this->messagedata->subject,
'objectid' => $this->optionid ?? 0,
'message' => $this->messagedata->fullmessage ?? '',
// Store the full html message as this is useful if the message every needs to be replayed or audited.
'messagehtml' => $this->messagedata->fullmessagehtml ?? '',
'bookingruleid' => $this->ruleid ?? null,
],
]);
$event->trigger();
Expand Down Expand Up @@ -552,7 +629,7 @@ private function get_attachments(bool $updated = false): array {
// Generate ical attachments to go with the message. Check if ical attachments enabled.
if (get_config('booking', 'attachical')) {
$ical = new ical($this->bookingsettings, $this->optionsettings, $this->user, $this->bookingmanager, $updated);
$attachments = $ical->get_attachments(false);
$attachments = $ical->get_attachments($updated);
$attachname = $ical->get_name();
}
}
Expand Down
1 change: 1 addition & 0 deletions classes/task/send_mail_by_rule_adhoc.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ public function execute() {
$taskdata->duedate ?? 0,
$taskdata->price ?? 0,
$taskdata->rulejson ?? 0,
$taskdata->ruleid ?? 0 // Send the ruleid as rulejson often seems to not work.
);
} catch (Exception $e) {
if (get_config('booking', 'bookingdebugmode')) {
Expand Down
4 changes: 4 additions & 0 deletions lang/de/booking.php
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@
"Nutzer:innen können nur bis n Tage vor Kursstart stornieren. Negative Werte meinen n Tage NACH Kursstart."
beziehen soll.<br>Dadurch wird auch die <i>Serviceperiode</i> von Kursen im Warenkorb entsprechend festgelegt
(wenn Shopping Cart installiert ist). Dies betrifft auch die Ratenzahlung. Entfernen Sie das ausgewählte Semester, wenn Sie Kursstart anstelle von Semesterstart nutzen möchten.';
$string['cancelical'] = 'Abbrechen';
$string['cancellation'] = 'Stornierung';
$string['cancellationsettings'] = 'Stornierungseinstellungen ' . '<span class="badge bg-success text-light"><i class="fa fa-cogs" aria-hidden="true"></i> PRO</span>';
$string['cancelmyself'] = 'Wieder abmelden';
Expand Down Expand Up @@ -773,6 +774,7 @@
$string['coursestart'] = 'Starten';
$string['coursestarttime'] = 'Kursbeginn';
$string['createdbywunderbyte'] = 'Dieses Buchungsmodul wurde von der Wunderbyte GmbH entwickelt';
$string['createical'] = 'Erstellen';
$string['createnewbookingoption'] = 'Neue Buchungsoption';
$string['createnewbookingoptionfromtemplate'] = 'Neue Buchungsoption von Vorlage erstellen';
$string['createnewmoodlecourse'] = 'Erstelle neuen, leeren Moodle-Kurs';
Expand Down Expand Up @@ -2015,6 +2017,8 @@
$string['sendcopyofmailmessageprefix'] = 'Vorangestellter Text für die Nachricht';
$string['sendcopyofmailsubjectprefix'] = 'Vorangestellter Text für den Betreff';
$string['sendcustommsg'] = 'Persönliche Nachricht senden';
$string['sendical'] = 'Senden Sie die ICS-Datei als Anhang';
$string['sendicalcreateorcancel'] = 'Handelt es sich bei der iCal-Datei um eine Erstellung oder eine Stornierung eines Ereignisses?';
$string['sendmail'] = "Sende E-Mail";
$string['sendmailheading'] = 'E-Mail an alle TrainerInnen der ausgewählten Buchungsoptionen senden';
$string['sendmailinterval'] = 'Eine Nachricht zeitversetzt an mehrere Nutzer:innen schicken';
Expand Down
5 changes: 5 additions & 0 deletions lang/en/booking.php
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@
"Disallow users to cancel their booking n days before start. Minus means, that users can still cancel n
days AFTER course start.".<br>
This will also set the <i>service period</i> of courses in shopping cart accordingly (if shopping cart is installed). This also affects installment payments. You can take out the semester in the Dates section of a booking option, if you want to use the coursestart instead of the semesterstart.';
$string['cancelical'] = 'Cancel';
$string['cancellation'] = 'Cancellation';
$string['cancellationsettings'] = 'Cancellation settings ' . '<span class="badge bg-success text-light"><i class="fa fa-cogs" aria-hidden="true"></i> PRO</span>';
$string['cancelmyself'] = 'Undo my booking';
Expand Down Expand Up @@ -792,6 +793,7 @@
$string['coursestarttime'] = 'Start time of the course';
$string['courseurl'] = 'Course URL';
$string['createdbywunderbyte'] = 'Booking module created by Wunderbyte GmbH';
$string['createical'] = 'Create';
$string['createnewbookingoption'] = 'New booking option';
$string['createnewbookingoptionfromtemplate'] = 'Add a new booking option from template';
$string['createnewmoodlecourse'] = 'Create new empty Moodle course';
Expand Down Expand Up @@ -2077,13 +2079,16 @@
$string['sendcopyofmailmessageprefix'] = 'Message prefix for the copy';
$string['sendcopyofmailsubjectprefix'] = 'Subject prefix for the copy';
$string['sendcustommsg'] = 'Send custom message';
$string['sendical'] = 'Send ICS file as attachment';
$string['sendicalcreateorcancel'] = 'Is the Ical a create or cancel event?';
$string['sendmail'] = 'Send email';
$string['sendmailheading'] = 'Send mail to all teachers of selected bookingoption(s)';
$string['sendmailinterval'] = 'Send a message to multiple users with a time delay';
$string['sendmailtoallbookedusers'] = 'Send e-mail to all booked users';
$string['sendmailtobooker'] = 'Book other users page: Send mail to user who books instead to users who are booked';
$string['sendmailtobooker_help'] = 'Activate this option in order to send booking confirmation mails to the user who books other users instead to users, who have been added to a booking option. This is only relevant for bookings made on the page "book other users".';
$string['sendmailtoteachers'] = 'Send mail to teacher(s)';

$string['sendmessage'] = 'Send message';
$string['sendpollurltoteachers'] = 'Send poll url';
$string['sendreminderemail'] = "Send reminder e-mail";
Expand Down

0 comments on commit 9e8639d

Please sign in to comment.