diff --git a/lib/classes/message/manager.php b/lib/classes/message/manager.php index c016bd530c6..234cc93ab51 100644 --- a/lib/classes/message/manager.php +++ b/lib/classes/message/manager.php @@ -50,13 +50,19 @@ class manager { * * NOTE: to be used from message_send() only. * - * @param \stdClass $eventdata fully prepared event data for processors + * @param \stdClass|\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 'message' or 'message_read' table (false is not returned) */ - public static function send_message(\stdClass $eventdata, \stdClass $savemessage, array $processorlist) { + public static function send_message($eventdata, \stdClass $savemessage, array $processorlist) { global $CFG; + + if (!($eventdata instanceof \stdClass) && !($eventdata instanceof message)) { + // Not a valid object. + throw new \coding_exception('Message should be of type stdClass or \core\message\message'); + } + require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file. if (empty($processorlist)) { @@ -85,12 +91,13 @@ class manager { /** * Send message to message processors. * - * @param \stdClass $eventdata + * @param \stdClass|\core\message\message $eventdata * @param \stdClass $savemessage * @param array $processorlist * @return int $messageid */ - protected static function send_message_to_processors(\stdClass $eventdata, \stdClass $savemessage, array $processorlist) { + protected static function send_message_to_processors($eventdata, \stdClass $savemessage, array + $processorlist) { global $CFG, $DB; // We cannot communicate with external systems in DB transactions, @@ -114,7 +121,9 @@ class manager { $failed = false; foreach ($processorlist as $procname) { - if (!$processors[$procname]->object->send_message($eventdata)) { + // Let new messaging class add custom content based on the processor. + $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata; + if (!$processors[$procname]->object->send_message($proceventdata)) { debugging('Error calling message processor ' . $procname); $failed = true; // Previously the $messageid = false here was overridden diff --git a/lib/classes/message/message.php b/lib/classes/message/message.php new file mode 100644 index 00000000000..393d0e7527b --- /dev/null +++ b/lib/classes/message/message.php @@ -0,0 +1,252 @@ +. + +/** + * New messaging class. + * + * @package core_message + * @since Moodle 2.9 + * @copyright 2015 onwards Ankit Agarwal + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\message; + +defined('MOODLE_INTERNAL') || die(); + +/** + * New messaging class. + * + * 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. + * replyto string An email address which can be used to send an reply. + * attachment stored_file File instance that needs to be sent as attachment. + * attachname string Name of the attachment. + * + * @package core_message + * @since Moodle 2.9 + * @copyright 2015 onwards Ankit Agarwal + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class message { + /** @var string Component name. */ + private $component; + + /** @var string Name. */ + private $name; + + /** @var object|int The user who is sending this message. */ + private $userfrom; + + /** @var object|int The user who is receiving from which is sending this message. */ + private $userto; + + /** @var string Subject of the message. */ + private $subject; + + /** @var string Complete message. */ + private $fullmessage; + + /** @var int Message format. */ + private $fullmessageformat; + + /** @var string Complete message in html format. */ + private $fullmessagehtml; + + /** @var string Smaller version of the message. */ + private $smallmessage; + + /** @var int Is it a notification? */ + private $notification; + + /** @var string context url. */ + private $contexturl; + + /** @var string context name. */ + private $contexturlname; + + /** @var string An email address which can be used to send an reply. */ + private $replyto; + + /** @var int Used internally to store the id of the row representing this message in DB. */ + private $savedmessageid; + + /** @var \stored_file File to be attached to the message. Note:- not all processors support this.*/ + private $attachment; + + /** @var string Name of the attachment. Note:- not all processors support this.*/ + private $attachname; + + /** @var array a list of properties that is allowed for each message. */ + private $properties = array('component', 'name', 'userfrom', 'userto', 'subject', 'fullmessage', 'fullmessageformat', + 'fullmessagehtml', 'smallmessage', 'notification', 'contexturl', 'contexturlname', 'savedmessageid', + 'replyto', 'attachment', 'attachname'); + + /** @var array property to store any additional message processor specific content */ + private $additionalcontent = array(); + + /** + * Fullmessagehtml content including any processor specific content. + * + * @param string $processorname Name of the processor. + * + * @return mixed|string + */ + protected function get_fullmessagehtml($processorname = '') { + if (!empty($processorname) && isset($this->additionalcontent[$processorname])) { + return $this->get_message_with_additional_content($processorname, 'fullmessagehtml'); + } else { + return $this->fullmessagehtml; + } + } + + /** + * Fullmessage content including any processor specific content. + * + * @param string $processorname Name of the processor. + * + * @return mixed|string + */ + protected function get_fullmessage($processorname = '') { + if (!empty($processorname) && isset($this->additionalcontent[$processorname])) { + return $this->get_message_with_additional_content($processorname, 'fullmessage'); + } else { + return $this->fullmessage; + } + } + + /** + * Smallmessage content including any processor specific content. + * + * @param string $processorname Name of the processor. + * + * @return mixed|string + */ + protected function get_smallmessage($processorname = '') { + if (!empty($processorname) && isset($this->additionalcontent[$processorname])) { + return $this->get_message_with_additional_content($processorname, 'smallmessage'); + } else { + return $this->smallmessage; + } + } + + /** + * Helper method used to get message content added with processor specific content. + * + * @param string $processorname Name of the processor. + * @param string $messagetype one of 'fullmessagehtml', 'fullmessage', 'smallmessage'. + * + * @return mixed|string + */ + protected function get_message_with_additional_content($processorname, $messagetype) { + $message = $this->$messagetype; + if (isset($this->additionalcontent[$processorname]['*'])) { + // Content that needs to be added to all format. + $pattern = $this->additionalcontent[$processorname]['*']; + $message = empty($pattern['header']) ? $message : $pattern['header'] . $message; + $message = empty($pattern['footer']) ? $message : $message . $pattern['footer']; + } + + if (isset($this->additionalcontent[$processorname][$messagetype])) { + // Content that needs to be added to the specific given format. + $pattern = $this->additionalcontent[$processorname][$messagetype]; + $message = empty($pattern['header']) ? $message : $pattern['header'] . $message; + $message = empty($pattern['footer']) ? $message : $message . $pattern['footer']; + } + + return $message; + } + + /** + * Magic getter method. + * + * @param string $prop name of property to get. + * + * @return mixed + * @throws \coding_exception + */ + public function __get($prop) { + if (in_array($prop, $this->properties)) { + return $this->$prop; + } + throw new \coding_exception("Invalid property $prop specified"); + } + + /** + * Magic setter method. + * + * @param string $prop name of property to set. + * @param mixed $value value to assign to the property. + * + * @return mixed + * @throws \coding_exception + */ + public function __set($prop, $value) { + if (in_array($prop, $this->properties)) { + return $this->$prop = $value; + } + throw new \coding_exception("Invalid property $prop specified"); + } + + /** + * This method lets you define content that would be added to the message only for specific message processors. + * + * Example of $content:- + * array('fullmessagehtml' => array('header' => 'header content', 'footer' => 'footer content'), + * 'smallmessage' => array('header' => 'header content for small message', 'footer' => 'footer content'), + * '*' => array('header' => 'header content for all types', 'footer' => 'footer content') + * ) + * + * @param string $processorname name of the processor. + * @param array $content content to add in the above defined format. + */ + public function set_additional_content($processorname, $content) { + $this->additionalcontent[$processorname] = $content; + } + + /** + * Get a event object for a specific processor in stdClass format. + * + * @param string $processorname Name of the processor. + * + * @return \stdClass event object in stdClass format. + */ + public function get_eventobject_for_processor($processorname) { + // This is done for Backwards compatibility. We should consider throwing notices here in future versions and requesting + // them to use proper api. + + $eventdata = new \stdClass(); + foreach ($this->properties as $prop) { + $func = "get_$prop"; + $eventdata->$prop = method_exists($this, $func) ? $this->$func($processorname) : $this->$prop; + } + return $eventdata; + } +} \ No newline at end of file diff --git a/lib/messagelib.php b/lib/messagelib.php index 8f9ee6a25a5..d700bcabac7 100644 --- a/lib/messagelib.php +++ b/lib/messagelib.php @@ -51,7 +51,7 @@ require_once(dirname(dirname(__FILE__)) . '/message/lib.php'); * earlier versions did not do it consistently either. * * @category message - * @param stdClass $eventdata information about the message (component, userfrom, userto, ...) + * @param stdClass|\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 with submitted data */ function message_send($eventdata) { diff --git a/lib/tests/message_test.php b/lib/tests/message_test.php new file mode 100644 index 00000000000..3713aacc6ab --- /dev/null +++ b/lib/tests/message_test.php @@ -0,0 +1,204 @@ +. + +/** + * Test classes for \core\message\message. + * + * @package core_message + * @category test + * @copyright 2015 onwards Ankit Agarwal + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/** + * Test script for message class. + * + * @package core_message + * @category test + * @copyright 2015 onwards Ankit Agarwal + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_message_testcase extends advanced_testcase { + + /** + * Test the method get_eventobject_for_processor(). + */ + public function test_get_eventobject_for_processor() { + global $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + $user = $this->getDataGenerator()->create_user(); + + $message = new \core\message\message(); + $message->component = 'moodle'; + $message->name = 'instantmessage'; + $message->userfrom = $USER; + $message->userto = $user; + $message->subject = 'message subject 1'; + $message->fullmessage = 'message body'; + $message->fullmessageformat = FORMAT_MARKDOWN; + $message->fullmessagehtml = '

message body

'; + $message->smallmessage = 'small message'; + $message->notification = '0'; + $message->contexturl = 'http://GalaxyFarFarAway.com'; + $message->contexturlname = 'Context name'; + $message->replyto = "random@random.com"; + $message->attachname = 'attachment'; + $content = array('*' => array('header' => ' test ', 'footer' => ' test ')); // Extra content for all types of messages. + $message->set_additional_content('test', $content); + + // Create a file instance. + $usercontext = context_user::instance($user->id); + $file = new stdClass; + $file->contextid = $usercontext->id; + $file->component = 'user'; + $file->filearea = 'private'; + $file->itemid = 0; + $file->filepath = '/'; + $file->filename = '1.txt'; + $file->source = 'test'; + + $fs = get_file_storage(); + $file = $fs->create_file_from_string($file, 'file1 content'); + $message->attachment = $file; + + $stdclass = $message->get_eventobject_for_processor('test'); + + $this->assertSame($message->component, $stdclass->component); + $this->assertSame($message->name, $stdclass->name); + $this->assertSame($message->userfrom, $stdclass->userfrom); + $this->assertSame($message->userto, $stdclass->userto); + $this->assertSame($message->subject, $stdclass->subject); + $this->assertSame(' test ' . $message->fullmessage . ' test ', $stdclass->fullmessage); + $this->assertSame(' test ' . $message->fullmessagehtml . ' test ', $stdclass->fullmessagehtml); + $this->assertSame(' test ' . $message->smallmessage . ' test ', $stdclass->smallmessage); + $this->assertSame($message->notification, $stdclass->notification); + $this->assertSame($message->contexturl, $stdclass->contexturl); + $this->assertSame($message->contexturlname, $stdclass->contexturlname); + $this->assertSame($message->replyto, $stdclass->replyto); + $this->assertSame($message->attachname, $stdclass->attachname); + + // Extra content for fullmessage only. + $content = array('fullmessage' => array('header' => ' test ', 'footer' => ' test ')); + $message->set_additional_content('test', $content); + $stdclass = $message->get_eventobject_for_processor('test'); + $this->assertSame(' test ' . $message->fullmessage . ' test ', $stdclass->fullmessage); + $this->assertSame($message->fullmessagehtml, $stdclass->fullmessagehtml); + $this->assertSame($message->smallmessage, $stdclass->smallmessage); + + // Extra content for fullmessagehtml and smallmessage only. + $content = array('fullmessagehtml' => array('header' => ' test ', 'footer' => ' test '), + 'smallmessage' => array('header' => ' testsmall ', 'footer' => ' testsmall ')); + $message->set_additional_content('test', $content); + $stdclass = $message->get_eventobject_for_processor('test'); + $this->assertSame($message->fullmessage, $stdclass->fullmessage); + $this->assertSame(' test ' . $message->fullmessagehtml . ' test ', $stdclass->fullmessagehtml); + $this->assertSame(' testsmall ' . $message->smallmessage . ' testsmall ', $stdclass->smallmessage); + + // Extra content for * and smallmessage. + $content = array('*' => array('header' => ' test ', 'footer' => ' test '), + 'smallmessage' => array('header' => ' testsmall ', 'footer' => ' testsmall ')); + $message->set_additional_content('test', $content); + $stdclass = $message->get_eventobject_for_processor('test'); + $this->assertSame(' test ' . $message->fullmessage . ' test ', $stdclass->fullmessage); + $this->assertSame(' test ' . $message->fullmessagehtml . ' test ', $stdclass->fullmessagehtml); + $this->assertSame(' testsmall ' . ' test ' . $message->smallmessage . ' test ' . ' testsmall ', $stdclass->smallmessage); + } + + /** + * Test sending messages as email works with the new class. + */ + public function test_send_message() { + global $DB, $CFG; + $this->preventResetByRollback(); + $this->resetAfterTest(); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + // Test basic email processor. + $this->assertFileExists("$CFG->dirroot/message/output/email/version.php"); + $this->assertFileExists("$CFG->dirroot/message/output/popup/version.php"); + + $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'"); + set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2); + + // Extra content for all types of messages. + $message = new \core\message\message(); + $message->component = 'moodle'; + $message->name = 'instantmessage'; + $message->userfrom = $user1; + $message->userto = $user2; + $message->subject = 'message subject 1'; + $message->fullmessage = 'message body'; + $message->fullmessageformat = FORMAT_MARKDOWN; + $message->fullmessagehtml = '

message body

'; + $message->smallmessage = 'small message'; + $message->notification = '0'; + $content = array('*' => array('header' => ' test ', 'footer' => ' test ')); + $message->set_additional_content('email', $content); + + $sink = $this->redirectEmails(); + $messageid = message_send($message); + $emails = $sink->get_messages(); + $this->assertCount(1, $emails); + $email = reset($emails); + $recordexists = $DB->record_exists('message_read', array('id' => $messageid)); + $this->assertSame(true, $recordexists); + $this->assertSame($user1->email, $email->from); + $this->assertSame($user2->email, $email->to); + $this->assertSame($message->subject, $email->subject); + $this->assertNotEmpty($email->header); + $this->assertNotEmpty($email->body); + $this->assertRegExp('/test message body test/', $email->body); + $sink->clear(); + + // Extra content for small message only. Shouldn't show up in emails as we sent fullmessage and fullmessagehtml only in + // the emails. + $message = new \core\message\message(); + $message->component = 'moodle'; + $message->name = 'instantmessage'; + $message->userfrom = $user1; + $message->userto = $user2; + $message->subject = 'message subject 1'; + $message->fullmessage = 'message body'; + $message->fullmessageformat = FORMAT_MARKDOWN; + $message->fullmessagehtml = '

message body

'; + $message->smallmessage = 'small message'; + $message->notification = '0'; + $content = array('smallmessage' => array('header' => ' test ', 'footer' => ' test ')); + $message->set_additional_content('email', $content); + + $messageid = message_send($message); + $emails = $sink->get_messages(); + $this->assertCount(1, $emails); + $email = reset($emails); + $recordexists = $DB->record_exists('message_read', array('id' => $messageid)); + $this->assertSame(true, $recordexists); + $this->assertSame($user1->email, $email->from); + $this->assertSame($user2->email, $email->to); + $this->assertSame($message->subject, $email->subject); + $this->assertNotEmpty($email->header); + $this->assertNotEmpty($email->body); + $this->assertNotRegExp('/test message body test/', $email->body); + $sink->close(); + } +} \ No newline at end of file diff --git a/mod/forum/lib.php b/mod/forum/lib.php index 8bb1995df13..6f96d019341 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -775,7 +775,7 @@ function forum_cron() { // Send the post now! mtrace('Sending ', ''); - $eventdata = new stdClass(); + $eventdata = new \core\message\message(); $eventdata->component = 'mod_forum'; $eventdata->name = 'posts'; $eventdata->userfrom = $userfrom; @@ -786,6 +786,14 @@ function forum_cron() { $eventdata->fullmessagehtml = $posthtml; $eventdata->notification = 1; $eventdata->replyto = $replyaddress; + if (!empty($replyaddress)) { + // Add extra text to email messages if they can reply back. + $textfooter = "\n\n" . get_string('replytopostbyemail', 'mod_forum'); + $htmlfooter = html_writer::tag('p', get_string('replytopostbyemail', 'mod_forum')); + $additionalcontent = array('fullmessage' => array('footer' => $textfooter), + 'fullmessagehtml' => array('footer' => $htmlfooter)); + $eventdata->set_additional_content('email', $additionalcontent); + } // If forum_replytouser is not set then send mail using the noreplyaddress. if (empty($CFG->forum_replytouser)) { @@ -1238,10 +1246,6 @@ function forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfro $posttext .= get_string("digestmailpost", "forum"); $posttext .= ": {$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}\n"; - if ($replyaddress) { - $posttext .= "\n\n" . get_string('replytopostbyemail', 'mod_forum'); - } - return $posttext; } @@ -1296,10 +1300,6 @@ function forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfro } $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false); - if ($replyaddress) { - $posthtml .= html_writer::tag('p', get_string('replytopostbyemail', 'mod_forum')); - } - $footerlinks = array(); if ($canunsubscribe) { if (\mod_forum\subscriptions::is_subscribed($userto->id, $forum, null, $cm)) {