ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK{\Sz7gateway/aws/db/hooks.phpnu[. /** * Hook listener callbacks for aws sms gateway. * * @package smsgateway_aws * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $callbacks = [ [ 'hook' => \core_sms\hook\after_sms_gateway_form_hook::class, 'callback' => \smsgateway_aws\hook_listener::class . '::set_form_definition_for_aws_sms_gateway', ], ]; PK{\ w&gateway/aws/lang/en/smsgateway_aws.phpnu[. /** * Strings for component smsgateway_aws, language 'en'. * * @package smsgateway_aws * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['api_key'] = 'Access key'; $string['api_region'] = 'Amazon API gateway region'; $string['api_secret'] = 'Secret access key'; $string['aws_information'] = 'Complete the following fields using the information provided by AWS'; $string['aws_sns'] = 'AWS SNS'; $string['gateway'] = 'AWS service'; $string['pluginname'] = 'AWS'; $string['privacy:metadata'] = 'The AWS SMS gateway plugin does not store any personal data.'; $string['usecredchain'] = 'Find AWS credentials using the default provider chain'; PK{\~ ;;gateway/aws/version.phpnu[. /** * Version information for smsgateway_aws. * * @package smsgateway_aws * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'smsgateway_aws'; $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->maturity = MATURITY_STABLE; PK{\/#(gateway/aws/classes/privacy/provider.phpnu[. namespace smsgateway_aws\privacy; use core_privacy\local\metadata\null_provider; /** * Privacy Subsystem for smsgateway_aws implementing null_provider. * * @package smsgateway_aws * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore */ class provider implements null_provider { #[\Override] public static function get_reason(): string { return 'privacy:metadata'; } } PK{\4:  6gateway/aws/classes/local/aws_sms_service_provider.phpnu[. namespace smsgateway_aws\local; use core_sms\message_status; use stdClass; /** * AWS SMS service provider interface to provide a standard interface for different aws service providers like sns, sqs etc. * * @package smsgateway_aws * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface aws_sms_service_provider { /** * Sends an SMS message using the selected aws service provider. * * @param string $messagecontent the content to send in the SMS message. * @param string $phonenumber the destination for the message. * @return message_status Status of the message. */ public static function send_sms_message( string $messagecontent, string $phonenumber, stdclass $config, ): message_status; } PK{\M7 -gateway/aws/classes/local/service/aws_sns.phpnu[. namespace smsgateway_aws\local\service; use core_sms\message_status; use smsgateway_aws\helper; use smsgateway_aws\local\aws_sms_service_provider; use stdClass; /** * AWS SNS service provider. * * @package smsgateway_aws * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class aws_sns implements aws_sms_service_provider { /** * Include the required calls. */ private static function require(): void { global $CFG; require_once($CFG->libdir . '/aws-sdk/src/functions.php'); } #[\Override] public static function send_sms_message( string $messagecontent, string $phonenumber, stdclass $config, ): message_status { global $SITE; self::require(); // Setup client params and instantiate client. $params = [ 'version' => 'latest', 'region' => $config->api_region, 'http' => ['proxy' => helper::get_proxy_string()], ]; if (!property_exists($config, 'usecredchain') || !$config->usecredchain) { $params['credentials'] = [ 'key' => $config->api_key, 'secret' => $config->api_secret, ]; } $client = new \Aws\Sns\SnsClient($params); // Set up the sender information. $senderid = $SITE->shortname; // Remove spaces and non-alphanumeric characters from ID. $senderid = preg_replace("/[^A-Za-z0-9]/", '', trim($senderid)); // We have to truncate the senderID to 11 chars. $senderid = substr($senderid, 0, 11); try { // These messages need to be transactional. $client->SetSMSAttributes([ 'attributes' => [ 'DefaultSMSType' => 'Transactional', 'DefaultSenderID' => $senderid, ], ]); // Actually send the message. $client->publish([ 'Message' => $messagecontent, 'PhoneNumber' => $phonenumber, ]); return \core_sms\message_status::GATEWAY_SENT; } catch (\Aws\Exception\AwsException $e) { return \core_sms\message_status::GATEWAY_NOT_AVAILABLE; } } } PK{\npy %gateway/aws/classes/hook_listener.phpnu[. namespace smsgateway_aws; use core_sms\hook\after_sms_gateway_form_hook; /** * Hook listener for aws sms gateway. * * @package smsgateway_aws * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class hook_listener { /** * Hook listener for the sms gateway setup form. * * @param after_sms_gateway_form_hook $hook The hook to add to sms gateway setup. */ public static function set_form_definition_for_aws_sms_gateway(after_sms_gateway_form_hook $hook): void { if ($hook->plugin !== 'smsgateway_aws') { return; } $mform = $hook->mform; $mform->addElement('static', 'information', get_string('aws_information', 'smsgateway_aws')); $gateways = [ 'aws_sns' => get_string('aws_sns', 'smsgateway_aws'), ]; $mform->addElement( 'select', 'gateway', get_string('gateway', 'smsgateway_aws'), $gateways, ); $mform->setDefault( elementName: 'gateway', defaultValue: 'aws_sns', ); // Remove this if more aws gateway implemented, eg sqs. $mform->hardFreeze('gateway'); $mform->addElement( 'checkbox', 'usecredchain', get_string('usecredchain', 'smsgateway_aws'), ' ', ); $mform->setDefault( elementName: 'usecredchain', defaultValue: 0, ); $mform->addElement( 'text', 'api_key', get_string('api_key', 'smsgateway_aws'), 'maxlength="255" size="20"', ); $mform->setType('api_key', PARAM_TEXT); $mform->addRule('api_key', get_string('maximumchars', '', 255), 'maxlength', 255); $mform->setDefault( elementName: 'api_key', defaultValue: '', ); $mform->addElement( 'passwordunmask', 'api_secret', get_string('api_secret', 'smsgateway_aws'), 'maxlength="255" size="20"', ); $mform->setType('api_secret', PARAM_TEXT); $mform->addRule('api_secret', get_string('maximumchars', '', 255), 'maxlength', 255); $mform->setDefault( elementName: 'api_secret', defaultValue: '', ); $mform->addElement( 'text', 'api_region', get_string('api_region', 'smsgateway_aws'), 'maxlength="255" size="20"', ); $mform->setType('api_region', PARAM_TEXT); $mform->addRule('api_region', get_string('maximumchars', '', 255), 'maxlength', 255); $mform->setDefault( elementName: 'api_region', defaultValue: 'ap-southeast-2', ); } } PK{\bplMMgateway/aws/classes/helper.phpnu[. namespace smsgateway_aws; /** * AWS SMS gateway helpers. * * @package smsgateway_aws * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * This creates a proxy string suitable for use with the AWS SDK. * * @return string the string to use for proxy settings. */ public static function get_proxy_string(): string { global $CFG; $proxy = ''; if (empty($CFG->proxytype)) { return $proxy; } if ($CFG->proxytype === 'SOCKS5') { // If it is a SOCKS proxy, append the protocol info. $protocol = 'socks5://'; } else { $protocol = ''; } if (!empty($CFG->proxyhost)) { $proxy = $CFG->proxyhost; if (!empty($CFG->proxyport)) { $proxy .= ':'. $CFG->proxyport; } if (!empty($CFG->proxyuser) && !empty($CFG->proxypassword)) { $proxy = $protocol . $CFG->proxyuser . ':' . $CFG->proxypassword . '@' . $proxy; } } return $proxy; } } PK{\ƞ gateway/aws/classes/gateway.phpnu[. namespace smsgateway_aws; use core_sms\manager; use core_sms\message; use MoodleQuickForm; /** * AWS SMS gateway. * * @package smsgateway_aws * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gateway extends \core_sms\gateway { #[\Override] public function send( message $message, ): message { global $DB; // Get the config from the message record. $awsconfig = $DB->get_field( table: 'sms_gateways', return: 'config', conditions: ['id' => $message->gatewayid, 'enabled' => 1, 'gateway' => 'smsgateway_aws\gateway',], ); $status = \core_sms\message_status::GATEWAY_NOT_AVAILABLE; if ($awsconfig) { $config = (object)json_decode($awsconfig, true, 512, JSON_THROW_ON_ERROR); $class = '\smsgateway_aws\local\service\\' . $config->gateway; $recipientnumber = manager::format_number( phonenumber: $message->recipientnumber, countrycode: isset($config->countrycode) ?? null, ); if (class_exists($class)) { $status = call_user_func( $class . '::send_sms_message', $message->content, $recipientnumber, $config, ); } } return $message->with( status: $status, ); } #[\Override] public function get_send_priority(message $message): int { return 50; } } PK{\&m !gateway/aws/tests/helper_test.phpnu[. namespace smsgateway_aws; /** * AWS SMS gateway helper tests. * * @package smsgateway_aws * @category test * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \smsgateway_aws\helper */ final class helper_test extends \advanced_testcase { /** * Data provider for test_format_number(). * * @return array of different country codes and phone numbers. */ public static function format_number_provider(): array { return [ 'Phone number with local format' => [ 'phonenumber' => '0123456789', 'expected' => '+34123456789', 'countrycode' => '34', ], 'Phone number with international format' => [ 'phonenumber' => '+39123456789', 'expected' => '+39123456789', ], 'Phone number with spaces using international format' => [ 'phonenumber' => '+34 123 456 789', 'expected' => '+34123456789', ], 'Phone number with spaces using local format with country code' => [ 'phonenumber' => '0 123 456 789', 'expected' => '+34123456789', 'countrycode' => '34', ], 'Phone number with spaces using local format without country code' => [ 'phonenumber' => '0 123 456 789', 'expected' => '123456789', ], ]; } /** * Test format number with different phones and different country codes. * * @dataProvider format_number_provider * @param string $phonenumber Phone number. * @param string $expected Expected value. * @param string|null $countrycode Country code. */ public function test_format_number( string $phonenumber, string $expected, ?string $countrycode = null, ): void { $this->resetAfterTest(); $this->assertEquals($expected, \core_sms\manager::format_number($phonenumber, $countrycode)); } /** * Test the proxy strings. */ public function test_get_proxy_string(): void { global $CFG; $this->resetAfterTest(); // Confirm with no config an empty string is returned. $CFG->proxyhost = ''; $this->assertEquals('', helper::get_proxy_string()); // Now set some configs. $CFG->proxyhost = '127.0.0.1'; $CFG->proxyuser = 'user'; $CFG->proxypassword = 'password'; $CFG->proxyport = '1337'; $this->assertEquals('user:password@127.0.0.1:1337', helper::get_proxy_string()); // Now change to SOCKS proxy. $CFG->proxytype = 'SOCKS5'; $this->assertEquals('socks5://user:password@127.0.0.1:1337', helper::get_proxy_string()); } } PK{\`$ "gateway/aws/tests/gateway_test.phpnu[. namespace smsgateway_aws; use core_sms\message; /** * AWS SMS gateway tests. * * @package smsgateway_aws * @category test * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \smsgateway_aws\gateway */ final class gateway_test extends \advanced_testcase { public function test_update_message_status(): void { $this->resetAfterTest(); $config = new \stdClass(); $config->api_key = 'test_api_key'; $manager = \core\di::get(\core_sms\manager::class); $gw = $manager->create_gateway_instance( classname: gateway::class, name: 'aws', enabled: true, config: $config, ); $othergw = $manager->create_gateway_instance( classname: gateway::class, name: 'aws', enabled: true, config: $config, ); $message = new message( recipientnumber: '1234567890', content: 'Hello, world!', component: 'smsgateway_aws', messagetype: 'test', recipientuserid: null, issensitive: false, gatewayid: $gw->id, ); $message2 = new message( recipientnumber: '1234567890', content: 'Hello, world!', component: 'smsgateway_aws', messagetype: 'test', recipientuserid: null, issensitive: false, gatewayid: $gw->id, ); $updatedmessages = $gw->update_message_statuses([$message, $message2]); $this->assertEquals([$message, $message2], $updatedmessages); $this->expectException(\coding_exception::class); $othergw->update_message_status($message); } } PK{\#%gateway/modica/db/hooks.phpnu[. /** * Hook listener callbacks for Modica sms gateway. * * @package smsgateway_modica * @copyright 2025 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $callbacks = [ [ 'hook' => \core_sms\hook\after_sms_gateway_form_hook::class, 'callback' => \smsgateway_modica\hook_listener::class . '::set_form_definition_for_modica_sms_gateway', ], ]; PK{\"Ə<<,gateway/modica/lang/en/smsgateway_modica.phpnu[. /** * Strings for component smsgateway_modica, language 'en'. * * @package smsgateway_modica * @copyright 2025 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['modica_application_name'] = 'Modica application name'; $string['modica_application_password'] = 'Modica application password'; $string['modica_information'] = 'Complete the following fields using the information provided by Modica'; $string['modica_url'] = 'Modica API URL'; $string['pluginname'] = 'Modica'; $string['privacy:metadata'] = 'The Modica SMS gateway plugin does not store any personal data.'; PK{\DDgateway/modica/version.phpnu[. /** * Version information for smsgateway_modica. * * @package smsgateway_modica * @copyright 2025 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'smsgateway_modica'; $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->maturity = MATURITY_STABLE; PK{\Z&+gateway/modica/classes/privacy/provider.phpnu[. namespace smsgateway_modica\privacy; use core_privacy\local\metadata\null_provider; /** * Privacy Subsystem for smsgateway_modica implementing null_provider. * * @package smsgateway_modica * @copyright 2025 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore */ class provider implements null_provider { #[\Override] public static function get_reason(): string { return 'privacy:metadata'; } } PK{\Nx (gateway/modica/classes/hook_listener.phpnu[. namespace smsgateway_modica; use core_sms\hook\after_sms_gateway_form_hook; /** * Hook listener for Modica sms gateway. * * @package smsgateway_modica * @copyright 2025 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class hook_listener { /** * Hook listener for the sms gateway setup form. * * @param after_sms_gateway_form_hook $hook The hook to add to sms gateway setup. */ public static function set_form_definition_for_modica_sms_gateway(after_sms_gateway_form_hook $hook): void { if ($hook->plugin !== 'smsgateway_modica') { return; } $mform = $hook->mform; $mform->addElement('static', 'information', get_string('modica_information', 'smsgateway_modica')); $mform->addElement( 'text', 'modica_url', get_string('modica_url', 'smsgateway_modica'), 'maxlength="255" size="20"', ); $mform->setType('modica_url', PARAM_URL); $mform->addRule('modica_url', get_string('maximumchars', '', 255), 'maxlength', 255); $mform->addRule('modica_url', null, 'required'); $mform->setDefault( elementName: 'modica_url', defaultValue: gateway::MODICA_DEFAULT_API, ); $mform->addElement( 'text', 'modica_application_name', get_string('modica_application_name', 'smsgateway_modica'), 'maxlength="255" size="20"', ); $mform->setType('modica_application_name', PARAM_TEXT); $mform->addRule('modica_application_name', get_string('maximumchars', '', 255), 'maxlength', 255); $mform->addRule('modica_application_name', null, 'required'); $mform->setDefault( elementName: 'modica_application_name', defaultValue: '', ); $mform->addElement( 'passwordunmask', 'modica_application_password', get_string('modica_application_password', 'smsgateway_modica'), 'maxlength="255" size="20"', ); $mform->setType('modica_application_password', PARAM_TEXT); $mform->addRule('modica_application_password', get_string('maximumchars', '', 255), 'maxlength', 255); $mform->addRule('modica_application_password', null, 'required'); $mform->setDefault( elementName: 'modica_application_password', defaultValue: '', ); } } PK{\ R R "gateway/modica/classes/gateway.phpnu[. namespace smsgateway_modica; use core\http_client; use core_sms\manager; use core_sms\message; use GuzzleHttp\Exception\GuzzleException; /** * Modica SMS gateway. * * @see https://confluence.modicagroup.com/display/DC/Mobile+Gateway+REST+API#MobileGatewayRESTAPI-Sendingtoasingledestination * @package smsgateway_modica * @copyright 2025 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gateway extends \core_sms\gateway { /** * @var string MODICA_DEFAULT_API The default api gateway for modica. */ public const MODICA_DEFAULT_API = 'https://api.modicagroup.com/rest/gateway/messages'; #[\Override] public function send( message $message, ): message { $recipientnumber = manager::format_number( phonenumber: $message->recipientnumber, countrycode: $this->config->countrycode ?? null, ); $json = json_encode( [ 'destination' => $recipientnumber, 'content' => $message->content, ], JSON_THROW_ON_ERROR, ); $client = \core\di::get(http_client::class); try { $response = $client->post( uri: $config->modica_url ?? self::MODICA_DEFAULT_API, options: [ 'auth' => [ $this->config->modica_application_name, $this->config->modica_application_password, ], 'headers' => ['Content-Type' => 'application/json'], 'body' => $json, ], ); if ($response->getStatusCode() === 201) { $status = \core_sms\message_status::GATEWAY_SENT; } else { $status = \core_sms\message_status::GATEWAY_FAILED; } } catch (GuzzleException $e) { $status = \core_sms\message_status::GATEWAY_FAILED; } return $message->with( status: $status, ); } #[\Override] public function get_send_priority(message $message): int { return 50; } } PK{\2ޕ% % %gateway/modica/tests/gateway_test.phpnu[. namespace smsgateway_modica; use core_sms\message; use core_sms\message_status; use GuzzleHttp\Psr7\Response; /** * Modica SMS gateway tests. * * @package smsgateway_modica * @category test * @copyright 2025 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \smsgateway_modica\gateway */ final class gateway_test extends \advanced_testcase { public function test_send(): void { $this->resetAfterTest(); $config = (object) [ 'modica_url' => gateway::MODICA_DEFAULT_API, 'modica_application_name' => 'test_application', 'modica_application_password' => 'test_password', ]; $manager = \core\di::get(\core_sms\manager::class); $gw = $manager->create_gateway_instance( classname: gateway::class, name: 'modica', enabled: true, config: $config, ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // Append the response status. $mock->append(new Response( status: 201, )); $message = $manager->send( recipientnumber: '+447123456789', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, async: false, ); $this->assertInstanceOf(message::class, $message); $this->assertIsInt($message->id); $this->assertEquals(message_status::GATEWAY_SENT, $message->status); $this->assertEquals($gw->id, $message->gatewayid); $this->assertEquals('Hello, world!', $message->content); $storedmessage = $manager->get_message(['id' => $message->id]); $this->assertEquals($message, $storedmessage); // Now let's try with failed status. $mock->append(new Response( status: 200, )); $message = $manager->send( recipientnumber: '+447123456789', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, async: false, ); $this->assertEquals(message_status::GATEWAY_FAILED, $message->status); $this->assertEquals($gw->id, $message->gatewayid); } } PK{\*? T classes/message_status.phpnu[. namespace core_sms; use core\attribute_helper; /** * The general status of a message. Gateways are able to provide more specific statuses to supplement these. * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ enum message_status: string { #[description('status:unknown', 'core_sms')] #[status()] case UNKNOWN = 'unknown'; #[description('status:message_over_size', 'core_sms')] #[status(failed: true)] case MESSAGE_OVER_SIZE = 'message_over_size'; #[description('status:gateway_not_available', 'core_sms')] #[status(failed: true)] case GATEWAY_NOT_AVAILABLE = 'gateway_not_available'; #[description('status:gateway_queued', 'core_sms')] #[status(inprogress: true)] case GATEWAY_QUEUED = 'gateway_queued'; #[description('status:gateway_sent', 'core_sms')] #[status(sent: true)] case GATEWAY_SENT = 'gateway_sent'; #[description('status:gateway_rejected', 'core_sms')] #[status(failed: true)] case GATEWAY_REJECTED = 'gateway_rejected'; #[description('status:gateway_failed', 'core_sms')] #[status(failed: true)] case GATEWAY_FAILED = 'gateway_failed'; /** * Whether the message is in a state marked as sent. * * @return bool */ public function is_sent(): bool { $status = attribute_helper::instance($this, status::class); return $status?->sent ?? false; } /** * Whether the message is in a state marked as failed. * * @return bool */ public function is_failed(): bool { $status = attribute_helper::instance($this, status::class); return $status?->failed ?? false; } /** * Whether the message is in an in-progress state. * * @return bool */ public function is_in_progress(): bool { $status = attribute_helper::instance($this, status::class); return $status?->inprogress ?? false; } /** * Get the human-readable status of the message. * * @return null|\lang_string */ public function description(): ?\lang_string { return attribute_helper::instance($this, description::class); } } PK{\,;;!classes/form/sms_gateway_form.phpnu[. namespace core_sms\form; use moodleform; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); /** * SMS gateway instance form. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sms_gateway_form extends moodleform { protected function definition() { global $PAGE; $PAGE->requires->js_call_amd('core_sms/smsgatewaychooser', 'init'); $mform = $this->_form; $gatewayconfigs = $this->_customdata['gatewayconfigs'] ?? []; $returnurl = $this->_customdata['returnurl'] ?? null; $smsplugins = []; $smsgatewayplugins = \core\plugininfo\smsgateway::get_enabled_plugins(); foreach ($smsgatewayplugins as $pluginname => $notusing) { $plugin = 'smsgateway_' . $pluginname; $smsplugins[$plugin] = get_string('pluginname', $plugin); } $mform->addElement( 'select', 'smsgateway', get_string('select_sms_gateways', 'sms'), $smsplugins, ['data-smsgatewaychooser-field' => 'selector'], ); $mform->setDefault('smsgateway', 'smsgateway_aws'); if (isset($gatewayconfigs->id)) { $mform->hardFreeze('smsgateway'); } $mform->addElement( 'text', 'name', get_string('gateway_name', 'sms'), 'maxlength="255" size="20"', ); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', get_string('required'), 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255); $mform->addElement( 'text', 'countrycode', get_string('countrycode', 'sms'), 'maxlength="255" size="20"', ); $mform->setType('countrycode', PARAM_TEXT); $mform->setDefault( elementName: 'countrycode', defaultValue: 0, ); $mform->addElement('static', 'information', ' ', get_string('countrycode_help', 'core_sms')); $mform->registerNoSubmitButton('updatesmsgateway'); $mform->addElement( 'submit', 'updatesmsgateway', 'update sms gateway', ['data-smsgatewaychooser-field' => 'updateButton', 'class' => 'd-none'] ); // Dispatch a hook for plugins to add their fields. $hook = new \core_sms\hook\after_sms_gateway_form_hook( mform: $mform, plugin: $gatewayconfigs->smsgateway ?? 'smsgateway_aws', ); \core\di::get(\core\hook\manager::class)->dispatch($hook); // Form buttons. $buttonarray = []; $buttonarray[] = $mform->createElement('submit', 'saveandreturn', get_string('savechanges')); $buttonarray[] = $mform->createElement('cancel'); $mform->addGroup($buttonarray, 'buttonar', '', [' '], false); $mform->closeHeaderBefore('buttonar'); if ($returnurl) { $mform->addElement('hidden', 'returnurl', $returnurl); $mform->setType('returnurl', PARAM_LOCALURL); } if (isset($gatewayconfigs->id)) { $mform->addElement('hidden', 'id', $gatewayconfigs->id); $mform->setType('id', PARAM_INT); } $this->set_data($gatewayconfigs); } } PK{\"Nuu'classes/external/sms_gateway_status.phpnu[. namespace core_sms\external; use context_system; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; /** * Webservice to enable or disable sms gateway. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sms_gateway_status extends external_api { public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'plugin' => new external_value(PARAM_INT, 'Gateway ID', VALUE_REQUIRED), 'state' => new external_value(PARAM_INT, 'Enabled or disabled', VALUE_REQUIRED), ]); } public static function execute(int $plugin, int $state): array { // Parameter validation. [ 'plugin' => $gatewayid, 'state' => $state, ] = self::validate_parameters(self::execute_parameters(), [ 'plugin' => $plugin, 'state' => $state, ]); $context = context_system::instance(); self::validate_context($context); require_capability('moodle/site:config', $context); $result = [ 'result' => true, 'message' => '', 'messagetype' => '', ]; $manager = \core\di::get(\core_sms\manager::class); $gatewaymanagers = $manager->get_gateway_instances(['id' => $gatewayid]); $gatewaymanager = reset($gatewaymanagers); if (!$gatewaymanager) { $result = [ 'result' => false, 'message' => 'sms_gateway_not_found', 'messagetype' => 'error' ]; return $result; } if (!empty($state)) { $manager->enable_gateway(gateway: $gatewaymanager); $message = get_string('plugin_enabled', 'core_admin', $gatewaymanager->name); $messagetype = \core\notification::SUCCESS; } else { $gatewayresult = $manager->disable_gateway(gateway: $gatewaymanager); if ($gatewayresult->enabled) { $result = [ 'result' => false, 'message' => 'sms_gateway_disable_failed', 'messagetype' => 'error' ]; $message = get_string('sms_gateway_disable_failed', 'core_sms'); $messagetype = \core\notification::ERROR; } else { $message = get_string('plugin_disabled', 'core_admin', $gatewaymanager->name); $messagetype = \core\notification::SUCCESS; } } \core\notification::add($message, $messagetype); return $result; } public static function execute_returns(): external_single_structure { return new external_single_structure( [ 'result' => new external_value(PARAM_BOOL, 'Whether the status was changed, true or false'), 'message' => new external_value(PARAM_TEXT, 'Messages'), 'messagetype' => new external_value(PARAM_TEXT, 'Message type'), ] ); } } PK{\ldXXclasses/privacy/provider.phpnu[. namespace message_sms\privacy; /** * Privacy provider for SMS processor. * * @package message_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { #[\Override] public static function get_reason(): string { return 'privacy:metadata'; } } PK{\77classes/manager.phpnu[. namespace core_sms; use Generator; use stdClass; /** * SMS manager. * * @package core_sms * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** * Create a new SMS manager. * * @param \moodle_database $db */ public function __construct( /** @var \moodle_database The database instance */ protected readonly \moodle_database $db, ) { } /** * Send an SMS to the given recipient. * * @param string $recipientnumber The phone number to send the SMS to * @param string $content The SMS Content * @param string $component The owning component * @param string $messagetype The message type within the component * @param null|int $recipientuserid The user id of the recipient if one exists * @param bool $issensitive Whether this SMS contains sensitive information * @param bool $async Whether this SMS should be sent asynchronously. Note: sensitive messages cannot be sent async * @param ?int $gatewayid the gateway instance id to send the sms in a specific gateway config * @return message * @throws \coding_exception If a sensitive message is sent asynchronously */ public function send( string $recipientnumber, string $content, string $component, string $messagetype, ?int $recipientuserid, bool $issensitive = false, bool $async = true, ?int $gatewayid = null, ): message { $message = new message( recipientnumber: $recipientnumber, content: $content, component: $component, messagetype: $messagetype, recipientuserid: $recipientuserid, issensitive: $issensitive, ); if ($issensitive && $async) { throw new \coding_exception('Sensitive messages cannot be sent asynchronously'); } return $this->send_message( message: $message, async: $async, gatewayid: $gatewayid, ); } /** * Send a message using the message object. * * @param message $message The message object * @param bool $async Whether this SMS should be sent asynchronously. Note: sensitive messages cannot be sent async * @param ?int $gatewayid the gateway instance id to send the sms in a specific gateway config * @return message the message object after trying to send the message */ public function send_message( message $message, bool $async = true, ?int $gatewayid = null, ): message { if ($async) { $message = $message->with(status: message_status::GATEWAY_QUEUED); $message = $this->save_message($message); \core_sms\task\send_sms_task::queue($message); return $message; } if ($gateway = $this->get_gateway_for_message($message, $gatewayid)) { $modifiedmessage = $message->with( gatewayid: $gateway->id, content: $gateway->truncate_message($message->content), ); $message = $gateway->send($modifiedmessage); } else { $message = $message->with(status: message_status::GATEWAY_NOT_AVAILABLE); } return $this->save_message($message); } /** * Get the gateways that can send the given message. * * @param message $message * @return gateway[] */ public function get_possible_gateways_for_message( message $message, ): array { $gateways = []; foreach ($this->get_enabled_gateway_instances() as $gateway) { if ($gateway->get_send_priority($message)) { $gateways[] = $gateway; } } return $gateways; } /** * Get the gateway that can send the given message. * * @param message $message The message instance * @param ?int $gatewayid the gateway instance id to send the sms in a specific gateway config * @return null|gateway */ public function get_gateway_for_message( message $message, ?int $gatewayid = null, ): ?gateway { if ($gatewayid) { // Check if the gateway id is valid. $gateway = $this->get_gateway_instances(['id' => $gatewayid]); $gateway = $gateway ? reset($gateway) : null; return $gateway; } $gateways = $this->get_possible_gateways_for_message($message); if (!count($gateways)) { return null; } // Sort the gateways by their send priority for this message. usort($gateways, fn($a, $b) => $a->get_send_priority($message) <=> $b->get_send_priority($message)); return array_pop($gateways); } /** * Get a list of all gateway instances. * * @param null|array $filter The database filter to apply * @return array */ public function get_gateway_instances(?array $filter = null): array { return array_filter( array_map( function ($record): ?gateway { if (!class_exists($record->gateway)) { debugging( "Unable to find a gateway class for {$record->gateway}", DEBUG_DEVELOPER, ); return null; } return new $record->gateway( id: $record->id, name: $record->name, enabled: $record->enabled, config: $record->config, ); }, $this->get_gateway_records($filter), ) ); } /** * Get the gateway records according to the filter. * * @param array|null $filter The filterable elements to get the records from * @return array */ public function get_gateway_records(?array $filter = null): array { return $this->db->get_records( table: 'sms_gateways', conditions: $filter, ); } /** * Get a list of all enabled gateway instances. * * @return array */ public function get_enabled_gateway_instances(): array { return $this->get_gateway_instances(['enabled' => 1]); } /** * Save the message to the database. * * @param message $message * @return message */ public function save_message( message $message, ): message { if ($message->issensitive) { // Sensitive messages should not store content. $message = $message->with(content: null); } if ($message->id) { $this->db->update_record('sms_messages', $message->to_record()); return $message; } $id = $this->db->insert_record('sms_messages', $message->to_record()); return $message->with(id: $id); } /** * Enable a gateway. * * @param gateway $gateway * @return gateway */ public function enable_gateway(gateway $gateway): gateway { if (!$gateway->enabled) { $gateway = $gateway->with(enabled: true); $this->db->update_record('sms_gateways', $gateway->to_record()); } return $gateway; } /** * Disable a gateway. * * @param gateway $gateway * @return gateway */ public function disable_gateway(gateway $gateway): gateway { if ($gateway->enabled) { try { $hook = new \core_sms\hook\before_gateway_disabled( gateway: $gateway, ); $hookmanager = \core\di::get(\core\hook\manager::class)->dispatch($hook); if (!$hookmanager->isPropagationStopped()) { $gateway = $gateway->with(enabled: false); $this->db->update_record('sms_gateways', $gateway->to_record()); } } catch (\dml_exception $e) { } } return $gateway; } /** * Delete the gateway instance. * * @param gateway $gateway The gateway instance * @return bool */ public function delete_gateway(gateway $gateway): bool { try { // Dispatch the hook before deleting the record. $hook = new \core_sms\hook\before_gateway_deleted( gateway: $gateway, ); $hookmanager = \core\di::get(\core\hook\manager::class)->dispatch($hook); if ($hookmanager->isPropagationStopped()) { $deleted = false; } else { $deleted = $this->db->delete_records('sms_gateways', ['id' => $gateway->id]); } } catch (\dml_exception $exception) { $deleted = false; } return $deleted; } /** * Create a new gateway instance. * * @param string $classname Classname of the gateway * @param string $name The name of the gateway config * @param bool $enabled If the gateway is enabled or not * @param stdClass|null $config The config json * @return gateway */ public function create_gateway_instance( string $classname, string $name, bool $enabled = false, ?stdClass $config = null, ): gateway { if (!class_exists($classname) || !is_a($classname, gateway::class, true)) { throw new \coding_exception("Gateway class not valid: {$classname}"); } $gateway = new $classname( enabled: $enabled, name: $name, config: $config ? json_encode($config) : '', ); $id = $this->db->insert_record('sms_gateways', $gateway->to_record()); return $gateway->with(id: $id); } /** * Update gateway instance. * * @param gateway $gateway The gateway instance * @param stdClass|null $config the configuration of the gateway instance to be updated * @return gateway */ public function update_gateway_instance( gateway $gateway, ?stdClass $config = null, ): gateway { $gateway = $gateway->with(config: $config, name: $gateway->name); $this->db->update_record('sms_gateways', $gateway->to_record()); return $gateway; } /** * Get all messages. * * @param string $sort * @param null|array $filter * @param int $pagesize * @param int $page * @return Generator */ public function get_messages( string $sort = 'timecreated ASC', ?array $filter = null, int $pagesize = 0, int $page = 0, ): Generator { $rows = $this->db->get_records( table: 'sms_messages', conditions: $filter, limitfrom: $pagesize * $page, limitnum: $pagesize, sort: $sort, ); foreach ($rows as $record) { yield new message( id: $record->id, recipientnumber: $record->recipientnumber, content: $record->content, component: $record->component, messagetype: $record->messagetype, recipientuserid: $record->recipientuserid, issensitive: $record->issensitive, status: message_status::from($record->status), gatewayid: $record->gatewayid, timecreated: $record->timecreated, ); } } /** * Get a message * * @param array $filter * @return message */ public function get_message( array $filter, ): message { $record = $this->db->get_record( table: 'sms_messages', conditions: $filter, ); return new message( id: $record->id, recipientnumber: $record->recipientnumber, content: $record->content, component: $record->component, messagetype: $record->messagetype, recipientuserid: $record->recipientuserid, issensitive: $record->issensitive, status: message_status::from($record->status), gatewayid: $record->gatewayid, timecreated: $record->timecreated, ); } /** * This function internationalises a number to E.164 standard. * https://46elks.com/kb/e164 * * @param string $phonenumber the phone number to format. * @param ?string $countrycode The country code of the phone number. * @return string the formatted phone number. */ public static function format_number( string $phonenumber, ?string $countrycode = null, ): string { // Remove all whitespace, dashes, and brackets in one step. $phonenumber = preg_replace('/[ ()-]/', '', $phonenumber); // Check if the number is already in international format or if it starts with a 0. if (!str_starts_with($phonenumber, '+')) { // Strip leading 0. if (str_starts_with($phonenumber, '0')) { $phonenumber = substr($phonenumber, 1); } // Prepend country code if not already in international format. $phonenumber = !empty($countrycode) ? '+' . $countrycode . $phonenumber : $phonenumber; } return $phonenumber; } } PK{\,'classes/hook/before_gateway_deleted.phpnu[. namespace core_sms\hook; use core_sms\gateway; use core\hook\stoppable_trait; /** * Hook before sms gateway is deleted. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read gateway $gateway The gateway instance */ #[\core\attribute\label('Allows plugins or features to check the usage of an SMS gateway before deleting the record.')] #[\core\attribute\tags('sms')] class before_gateway_deleted implements \Psr\EventDispatcher\StoppableEventInterface { use stoppable_trait; /** * Constructor for the hook. * * @param gateway $gateway The gateway instance. */ public function __construct( public readonly gateway $gateway, ) { } } PK{\ (classes/hook/before_gateway_disabled.phpnu[. namespace core_sms\hook; use core_sms\gateway; use core\hook\stoppable_trait; /** * Hook before sms gateway is disabled. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read gateway $gateway The gateway instance */ #[\core\attribute\label('Allows plugins or features to check the usage of an SMS gateway before disabling the gateway.')] #[\core\attribute\tags('sms')] class before_gateway_disabled implements \Psr\EventDispatcher\StoppableEventInterface { use stoppable_trait; /** * Constructor for the hook. * * @param gateway $gateway The gateway instance. */ public function __construct( public readonly gateway $gateway, ) { } } PK{\E,classes/hook/after_sms_gateway_form_hook.phpnu[. namespace core_sms\hook; use MoodleQuickForm; /** * Hook after sms gateway setup form is initiated. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read MoodleQuickForm $mform The form element * @property-read ?string $plugin Name of the plugin */ #[\core\attribute\label('Allows plugins or features to add form elements for sms gateway setup.')] #[\core\attribute\tags('sms')] class after_sms_gateway_form_hook { /** * Constructor for the hook. * * @param MoodleQuickForm $mform The moodle form instance. * @param ?string $plugin The name of the plugin */ public function __construct( public readonly MoodleQuickForm $mform, public readonly ?string $plugin, ) { } } PK{\nwclasses/message.phpnu[. namespace core_sms; use libphonenumber\NumberParseException; use Spatie\Cloneable\Cloneable; use ValueError; /** * A Message used in an SMS. * * Note: This class is immutable. All properties are readonly. * The class itself will likely become readonly when PHP 8.2 is the minimum requirement. * * @package core_sms * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read int $timecreated The time that the message was created * @property-read string $recipientnumber The recipient of the message * @property-read null|string $content The content of the message * @property-read string $component The component that owns the message * @property-read string $messagetype The type of message within the component * @property-read int|null $recipientuserid The user id of the recipient if one exists * @property-read bool $issensitive Whether this message contains sensitive information * @property-read int|null $id The id of the message in the database * @property-read message_status $status The status of the message * @property-read int|null $gatewayid The id of the gateway that sent the message */ class message { use Cloneable { with as private _with; } /** @var int The time that the message was created */ public readonly int $timecreated; /** * Create a new message. * * @param string $recipientnumber The phone number of the message recipient * @param null|string $content The content of the message * @param string $component The component that owns the message * @param string $messagetype The type of message within the component * @param int|null $recipientuserid The user id of the recipient if one exists * @param bool $issensitive Whether this message contains sensitive information * @param int|null $id The id of the message in the database * @param message_status $status The status of the message * @param int|null $gatewayid The id of the gateway that sent the message * @param int|null $timecreated The time that the message was created */ public function __construct( /** @var string The phone number of the message recipient */ public readonly string $recipientnumber, /** @var null|string The content of the message */ public readonly ?string $content, /** @var string The component that owns the message */ public readonly string $component, /** @var string The type of message within the component */ public readonly string $messagetype, /** @var int|null The user id of the recipient if one exists */ public readonly ?int $recipientuserid, /** @var bool Whether this message contains sensitive information */ public readonly bool $issensitive, /** @var null|int The id of the message in the database */ public readonly ?int $id = null, /** @var message_status The status of the message */ public readonly message_status $status = message_status::UNKNOWN, /** @var null|int The id of the gateway that sent the message */ public readonly ?int $gatewayid = null, ?int $timecreated = null, ) { if ($timecreated === null) { $this->timecreated = \core\di::get(\core\clock::class)->now()->getTimestamp(); } else { $this->timecreated = $timecreated; } } /** * Convert the message to a record. * * @return \stdClass */ public function to_record(): \stdClass { $record = (object) [ 'recipientnumber' => $this->recipientnumber, 'content' => $this->content, 'component' => $this->component, 'messagetype' => $this->messagetype, 'recipientuserid' => $this->recipientuserid, 'issensitive' => $this->issensitive, 'status' => $this->status->value, 'gatewayid' => $this->gatewayid, 'timecreated' => $this->timecreated, ]; if ($this->id !== null) { $record->id = $this->id; } return $record; } /** * Update the message properties. * * Note: The message is immutable. * When setting the status a new object will be returned. * * @param mixed ...$args * @return message */ public function with(...$args): self { if (isset($this->id) && array_key_exists('id', $args)) { throw new \coding_exception('Message already has an id'); } return $this->_with(...$args); } /** * Get the region code for this messages. * * @return null|string */ public function get_region(): ?string { $pnu = \core\di::get(\libphonenumber\PhoneNumberUtil::class); try { $recipient = $pnu->parse($this->recipientnumber); } catch (NumberParseException $e) { // Note: Throw errors which are not specific to libphonenumber here. // This is to avoid hard-trying use of this library. throw new ValueError( new \lang_string('phonenumbernotvalid', 'sms', ['message' => $e->getMessage()]), $e->getCode(), $e, ); } return $pnu->getRegionCodeForNumber($recipient); } /** * Check if the message has been sent. * * @return bool */ public function is_sent(): bool { return $this->status->is_sent(); } } PK{\7"classes/description.phpnu[. namespace core_sms; /** * A language string attribute used for a description. * * @package core_sms * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\Attribute] class description extends \lang_string { } PK{\+SSclasses/task/send_sms_task.phpnu[. namespace core_sms\task; use core\task\adhoc_task; use core_sms\message; /** * Ad-hoc task to send an SMS. * * Please note: sensitive SMS should not be sent from this task, it's already handled via the SMS api to exclude them. * * @package core_sms * @copyright 2025 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_sms_task extends adhoc_task { #[\Override] public function execute(): void { $smsdata = $this->get_custom_data(); $manager = \core\di::get(\core_sms\manager::class); $message = $manager->get_message( filter: ['id' => $smsdata->messageid], ); $manager->send_message( message: $message, async: false, ); } /** * Queue the SMS. * * @param message $message The message object. */ public static function queue( message $message, ): void { $task = new self(); $task->set_custom_data( [ 'messageid' => $message->id, ] ); \core\task\manager::queue_adhoc_task($task); } } PK{\Zi**#classes/table/sms_gateway_table.phpnu[. namespace core_sms\table; use context_system; use core_table\dynamic as dynamic_table; use flexible_table; use moodle_url; use stdClass; defined('MOODLE_INTERNAL') || die(); require_once("{$CFG->libdir}/tablelib.php"); /** * List sms gateway instances in a table. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sms_gateway_table extends flexible_table implements dynamic_table { /** * @var array $smsgateways List of gateway instances from the db. */ protected array $smsgateways = []; /** * Constructor for the sms gateway table. */ public function __construct() { parent::__construct($this->get_table_id()); $this->smsgateways = $this->get_sms_gateways(); $this->setup_column_configuration(); $this->set_filterset(new sms_gateway_table_filterset()); $this->setup(); $tableclasses = $this->attributes['class'] . ' ' . $this->get_table_id(); $this->set_attribute('class', $tableclasses); } #[\Override] public function get_context(): context_system { return context_system::instance(); } /** * Get the js module needed for the table. * * This module can include table specific ajax calls etc. * * @return string */ protected function get_table_js_module(): string { return 'core_admin/plugin_management_table'; } #[\Override] protected function get_dynamic_table_html_end(): string { global $PAGE; $PAGE->requires->js_call_amd($this->get_table_js_module(), 'init'); return parent::get_dynamic_table_html_end(); } /** * Setup the column configs for the table. */ protected function setup_column_configuration(): void { $columnlist = $this->get_column_list(); $this->define_columns(array_keys($columnlist)); $this->define_headers(array_values($columnlist)); } /** * Get the columns for the table. * * @return array */ protected function get_column_list(): array { return [ 'name' => get_string('name'), 'gateway' => get_string('gateway', 'sms'), 'enabled' => get_string('status'), 'actions' => get_string('actions', 'sms'), ]; } /** * Get the table id for the table. * * @return string */ protected function get_table_id(): string { return 'sms_gateways_table'; } #[\Override] public function guess_base_url(): void { $this->define_baseurl(new moodle_url('/sms/sms_gateways.php')); } /** * Get the content of the table. * * @return string */ public function get_content(): string { ob_start(); $this->out(); $content = ob_get_contents(); ob_end_clean(); return $content; } /** * Get the sms gateways from the manager. * * @return array */ protected function get_sms_gateways(): array { $gateways = \core\di::get(\core_sms\manager::class)->get_gateway_records(); if (!empty($gateways)) { \core_collator::asort_objects_by_property($gateways, 'id'); } return $gateways; } /** * Add the row data for the table. */ public function out(): void { foreach ($this->smsgateways as $gateway) { $rowdata = (object) [ 'id' => $gateway->id, 'name' => $gateway->name, 'gateway' => $gateway->gateway, 'enabled' => (int)$gateway->enabled, ]; $this->add_data_keyed( $this->format_row($rowdata), $this->get_row_class($rowdata) ); } $this->finish_output(false); } /** * Get the class for row whether is dimmed or not according to enabled or disabled. * * @param stdClass $row The row object * @return string */ protected function get_row_class(stdClass $row): string { if ($row->enabled) { return ''; } return 'dimmed_text'; } /** * The column for the gateway instance Name. * * @param stdClass $row The row object * @return string */ public function col_name(stdClass $row): string { return $row->name; } /** * The column for the Gateway plugin name. * * @param stdClass $row The row object * @return string */ public function col_gateway(stdClass $row): string { return $this->get_gateway_name($row->gateway); } /** * Get the gateway name according to the gateway class from db. * * @param string $gateway The name of the gateway * @return string */ public function get_gateway_name(string $gateway): string { $values = explode('\\', $gateway); $gateway = $values[0]; return get_string('pluginname', $gateway); } /** * Webservice for toggle. * * @return string */ protected function get_toggle_service(): string { return 'core_sms_set_gateway_status'; } /** * The column for enabled or disabled status of the gateway instance. * * @param stdClass $row The row object * @return string */ public function col_enabled(stdClass $row): string { global $OUTPUT; $enabled = $row->enabled; if ($enabled) { $labelstr = get_string('disableplugin', 'core_admin', $row->name); } else { $labelstr = get_string('enableplugin', 'core_admin', $row->name); } $params = [ 'id' => 'sms-gateway-toggle-' . $row->id, 'checked' => $enabled, 'dataattributes' => [ 'name' => 'id', 'value' => $this->get_gateway_name($row->gateway), 'toggle-method' => $this->get_toggle_service(), 'action' => 'togglestate', 'plugin' => $row->id, // Set plugin attribute to gateway ID. 'state' => $enabled ? 1 : 0, ], 'title' => $labelstr, 'label' => $labelstr, 'labelclasses' => 'visually-hidden', ]; return $OUTPUT->render_from_template('core_admin/setting_configtoggle', $params); } /** * The column to show the actions of the gateway instance. * * @param stdClass $row The row object. * @return string */ public function col_actions(stdClass $row): string { global $OUTPUT; $editurl = new moodle_url('/sms/configure.php', ['id' => $row->id]); $deleteurl = new moodle_url('/sms/sms_gateways.php', ['id' => $row->id, 'action' => 'delete']); $templatecontext = [ 'editurl' => $editurl->out(false), 'deleteurl' => $deleteurl->out(false), ]; return $OUTPUT->render_from_template('core_sms/sms_action_icons', $templatecontext); } #[\Override] public function has_capability(): bool { return has_capability('moodle/site:config', $this->get_context()); } } PK{\8-classes/table/sms_gateway_table_filterset.phpnu[. namespace core_sms\table; /** * SMS gateway table filter set. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sms_gateway_table_filterset extends \core_table\local\filter\filterset { } PK{\yclasses/gateway.phpnu[. namespace core_sms; use coding_exception; use Spatie\Cloneable\Cloneable; use stdClass; /** * Class gateway. * * @package core_sms * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read int $id The id of the gateway in the database * @property-read bool $enabled Whether the gateway is enabled * @property-read stdClass $config The configuration for this instance * @property string $name The name of the gateway config */ abstract class gateway { use Cloneable; /** @var int The maximum length of a message. */ protected const MESSAGE_LENGTH_LIMIT = 160 * 3; /** @var stdClass The configuration for this instance */ public readonly stdClass $config; /** * Create a new gateway. * * @param bool $enabled Whether the gateway is enabled * @param string $name The name of the gateway config * @param string $config The configuration for this instance * @param int|null $id The id of the gateway in the database */ public function __construct( /** @var bool Whether the gateway is enabled */ public readonly bool $enabled, /** @var string The name of the gateway config */ public string $name, string $config, /** @var null|int The ID of the gateway in the database, or null if it has not been persisted yet */ public readonly ?int $id = null, ) { $this->config = json_decode($config); } /** * Convert this object to a stdClass. * * @return stdClass */ public function to_record(): stdClass { return (object) [ 'id' => $this->id, 'name' => $this->name, 'gateway' => get_class($this), 'enabled' => $this->enabled, 'config' => json_encode($this->config), ]; } /** * Send the given message. * * @param message $message * @return message */ abstract public function send(message $message): message; /** * Confirm whether this gateway can send the given message. * * @param message $message * @return bool */ public function can_send(message $message): bool { return $this->get_send_priority($message) > 0; } /** * Get the priority of this gateway for sending the given message. * * A priority of 0 means that the gateway cannot send the message. * Higher values are higher priority. * * This method is called frequently, so should be fast. * If calculation is expensive the value should be cached. * * @param message $message * @return int */ abstract public function get_send_priority(message $message): int; /** * Update the status of the given message from the gateway. * * @param message $message * @return message * @throws coding_exception */ public function update_message_status(message $message): message { if ($message->gatewayid !== $this->id) { throw new \coding_exception('This gateway cannot update the status of this message'); } return $message; } /** * Update the statuses of the given messages from the gateway. * * @param message[] $messages * @return message[] */ public function update_message_statuses(array $messages): array { return array_map([$this, 'update_message_status'], $messages); } /** * Truncates the given message to fit the constraints. * * @param string $message The message to be truncated. * @return string The truncated message. */ public function truncate_message(string $message): string { return \core_text::substr($message, 0, static::MESSAGE_LENGTH_LIMIT); } } PK{\]쬑classes/status.phpnu[. namespace core_sms; /** * Message status categorisation. * * Statuses in the message_status class provide a human-readable machine description of the status. * * Each message status can further be categorised with attributes such as: * - sent * - failed * - inprogress * * @package core_sms * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\Attribute] class status { /** * Message status categorisation. * * @param bool $sent * @param bool $failed * @param bool $inprogress */ public function __construct( /** @var bool Whether the message was sent successfully */ public readonly bool $sent = false, /** @var bool Whether the message is in a failed state */ public readonly bool $failed = false, /** @var bool Whether the message is in-progress */ public readonly bool $inprogress = false, ) { } } PK{\ϳ[[#templates/sms_action_icons.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template core_sms/sms_action_icons Template for sms gateway actions icons. Context variables required for this template: * editurl - URL for the edit button * deleteurl - URL for the delete button Example context (json): { "editurl": "http://example.com/edit", "deleteurl": "http://example.com/delete" } }} PK{\=+templates/sms_gateways.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template core_sms/sms_gateways Template for sms gateways table. Context variables required for this template: * createurl - URL of the create button * tablehtml - HTML of the table of sms gateway Example context (json): { "createurl": "www.example.com", "tablehtml": "
GatewayStatusActions
" } }}
{{#str}}sms_gateways_info, sms{{/str}}
{{{tablehtml}}}
PK{\ڄamd/src/smsgatewaychooser.jsnu[ // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * SMS gateway selection handler. * * @module core_sms/smsgatewaychooser * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const Selectors = { fields: { selector: '[data-smsgatewaychooser-field="selector"]', updateButton: '[data-smsgatewaychooser-field="updateButton"]', }, }; /** * Initialise the sms gateway chooser. */ export const init = () => { document.querySelector(Selectors.fields.selector)?.addEventListener('change', e => { const form = e.target.closest('form'); const updateButton = form.querySelector(Selectors.fields.updateButton); const url = new URL(form.action); form.action = url.toString(); updateButton.click(); }); }; PK{\!  &amd/build/smsgatewaychooser.min.js.mapnu[{"version":3,"file":"smsgatewaychooser.min.js","sources":["../src/smsgatewaychooser.js"],"sourcesContent":["\n// This file is part of Moodle - http://moodle.org/ //\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * SMS gateway selection handler.\n *\n * @module core_sms/smsgatewaychooser\n * @copyright 2024 Safat Shahin \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst Selectors = {\n fields: {\n selector: '[data-smsgatewaychooser-field=\"selector\"]',\n updateButton: '[data-smsgatewaychooser-field=\"updateButton\"]',\n },\n};\n\n/**\n * Initialise the sms gateway chooser.\n */\nexport const init = () => {\n document.querySelector(Selectors.fields.selector)?.addEventListener('change', e => {\n const form = e.target.closest('form');\n const updateButton = form.querySelector(Selectors.fields.updateButton);\n const url = new URL(form.action);\n\n form.action = url.toString();\n updateButton.click();\n });\n};\n"],"names":["Selectors","selector","updateButton","document","querySelector","addEventListener","e","form","target","closest","url","URL","action","toString","click"],"mappings":";;;;;;;;MAuBMA,iBACM,CACJC,SAAU,4CACVC,aAAc,+DAOF,6DAChBC,SAASC,cAAcJ,iBAAiBC,kEAAWI,iBAAiB,UAAUC,UACpEC,KAAOD,EAAEE,OAAOC,QAAQ,QACxBP,aAAeK,KAAKH,cAAcJ,iBAAiBE,cACnDQ,IAAM,IAAIC,IAAIJ,KAAKK,QAEzBL,KAAKK,OAASF,IAAIG,WAClBX,aAAaY"}PK{\$w6"amd/build/smsgatewaychooser.min.jsnu[define("core_sms/smsgatewaychooser",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; /** * SMS gateway selection handler. * * @module core_sms/smsgatewaychooser * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const Selectors_fields={selector:'[data-smsgatewaychooser-field="selector"]',updateButton:'[data-smsgatewaychooser-field="updateButton"]'};_exports.init=()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector(Selectors_fields.selector))||void 0===_document$querySelect||_document$querySelect.addEventListener("change",(e=>{const form=e.target.closest("form"),updateButton=form.querySelector(Selectors_fields.updateButton),url=new URL(form.action);form.action=url.toString(),updateButton.click()}))}})); //# sourceMappingURL=smsgatewaychooser.min.js.mapPK{\C UPGRADING.mdnu[# core_sms (subsystem) Upgrade notes ## 5.0 ### Added - Introducing a new function \core_sms\gateway::truncate_message() to truncate SMS message content according to the length limit of the gateway. For more information see [MDL-84342](https://tracker.moodle.org/browse/MDL-84342) ## 4.5 ### Added - A new `\core_sms` subsystem has been created. For more information see [MDL-81924](https://tracker.moodle.org/browse/MDL-81924) PK{\~- sms_gateways.phpnu[. /** * List sms gateway instances. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\output\notification; require_once('../config.php'); require_once($CFG->dirroot . '/lib/adminlib.php'); require_login(); $context = context_system::instance(); require_capability('moodle/site:config', $context); $id = optional_param('id', null, PARAM_INT); $action = optional_param('action', '', PARAM_TEXT); $confirm = optional_param('confirm', 0, PARAM_BOOL); // Set up the page. $title = get_string('sms_gateways', 'sms'); $returnurl = new moodle_url('/sms/sms_gateways.php'); admin_externalpage_setup('smsgateway'); $PAGE->set_primary_active_tab('siteadminnode'); $PAGE->navbar->add($title, $returnurl); $PAGE->set_context($context); $PAGE->set_url($returnurl); $PAGE->set_title($title); $PAGE->set_heading($title); if (!empty($id) && !empty($action)) { $manager = \core\di::get(\core_sms\manager::class); $gatewayrecord = $manager->get_gateway_records(['id' => $id]); $gatewayrecord = reset($gatewayrecord); $pluginname = explode('\\', $gatewayrecord->gateway); $pluginname = $pluginname[0]; $gateway = $manager->get_gateway_instances(['id' => $id]); $gateway = reset($gateway); $a = new stdClass(); $a->gateway = $gateway->name; } if ($action === 'delete') { $PAGE->url->param('action', 'delete'); if ($confirm && confirm_sesskey()) { if ($manager->delete_gateway($gateway)) { $message = get_string('sms_gateway_deleted', 'sms', $a); $messagestyle = notification::NOTIFY_SUCCESS; } else { $message = get_string('sms_gateway_delete_failed', 'sms', $a); $messagestyle = notification::NOTIFY_ERROR; } redirect($returnurl, $message, null, $messagestyle); } $strheading = get_string('delete_sms_gateway', 'sms'); $PAGE->navbar->add($strheading); $PAGE->set_title($strheading); $PAGE->set_heading($strheading); echo $OUTPUT->header(); $yesurl = new moodle_url($returnurl, ['id' => $id, 'action' => 'delete', 'confirm' => 1]); $deletedisplay = [ 'confirmtitle' => get_string('deletecheck', '', $a->gateway), 'continuestr' => get_string('delete'), ]; $message = get_string('delete_sms_gateway_confirmation', 'sms', $a); echo $OUTPUT->confirm($message, $yesurl, $returnurl, $deletedisplay); echo $OUTPUT->footer(); die; } $table = new \core_sms\table\sms_gateway_table(); $templatecontext = new stdClass(); $templatecontext->tablehtml = $table->get_content(); $templatecontext->createurl = new moodle_url('/sms/configure.php'); echo $OUTPUT->header(); echo $OUTPUT->render_from_template('core_sms/sms_gateways', $templatecontext); echo $OUTPUT->footer(); PK{\Lvv tests/fixtures/dummy_gateway.phpnu[. namespace smsgateway_dummy; use core_sms\message; /** * Dummy SMS gateway. * * @package core_sms * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gateway extends \core_sms\gateway { /** @var int The maximum length of a message */ protected const MESSAGE_LENGTH_LIMIT = 160 * 1; #[\Override] public function send(message $message): message { return $message->with( status: \core_sms\message_status::GATEWAY_SENT, ); } #[\Override] public function get_send_priority(message $message): int { if (!$this->config) { return 50; } // Check if the recipient number starts with a specific prefix. if (property_exists($this->config, 'startswith')) { $startswith = substr($message->recipientnumber, 0, 3); if (property_exists($this->config->startswith, $startswith)) { return (int) $this->config->startswith->$startswith; } } if (property_exists($this->config, 'priority')) { return (int) $this->config->priority; } return 0; } } PK{\r%$$tests/generator/lib.phpnu[. use core_sms\gateway; /** * SMS data generator for tests. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_sms_generator extends component_generator_base { /** * Create sms gateway record. * * @param array $record The gateway record to be created. * @return gateway */ public function create_sms_gateways(array $record): gateway { $config = json_decode($record['config'], false, 512, JSON_THROW_ON_ERROR); $manager = \core\di::get(\core_sms\manager::class); $gateway = $manager->create_gateway_instance( classname: $record['classname'], name: $record['name'], enabled: $record['enabled'], config: $config, ); return $gateway; } } PK{\lT,tests/generator/behat_core_sms_generator.phpnu[. /** * Behat generator for SMS. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_core_sms_generator extends behat_generator_base { /** * Get the list of creatable entities for a core_sms. * * @return array */ protected function get_creatable_entities(): array { return [ 'sms_gateways' => [ 'singular' => 'sms_gateway', 'datagenerator' => 'sms_gateways', 'required' => [ 'classname', 'name', 'enabled', 'config', ], ], ]; } } PK{\& *tests/external/sms_gateway_status_test.phpnu[. declare(strict_types=1); namespace core_sms\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Test the webservice to change the status of a gateway. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_sms\external\sms_gateway_status::execute */ final class sms_gateway_status_test extends \externallib_advanced_testcase { public function test_execute(): void { $this->resetAfterTest(); $this->setAdminUser(); $config = (object) [ 'data' => 'goeshere', ]; $dummy = $this->getMockBuilder(\core_sms\gateway::class) ->setConstructorArgs([ 'enabled' => true, 'name' => 'dummy', 'config' => json_encode($config), ]) ->onlyMethods(['get_send_priority', 'send']) ->getMock(); $dummygw = get_class($dummy); $manager = \core\di::get(\core_sms\manager::class); $gateway = $manager->create_gateway_instance( classname: $dummygw, name: 'dummy', enabled: true, config: $config, ); // Now let's disable the gateway. sms_gateway_status::execute( plugin: $gateway->id, state: 0, ); $gatewaymanagers = $manager->get_gateway_instances( filter: ['id' => $gateway->id], ); $gatewaymanager = reset($gatewaymanagers); $this->assertFalse($gatewaymanager->enabled); // Let's enable again. sms_gateway_status::execute( plugin: $gateway->id, state: 1, ); $gatewaymanagers = $manager->get_gateway_instances( filter: ['id' => $gateway->id], ); $gatewaymanager = reset($gatewaymanagers); $this->assertTrue($gatewaymanager->enabled); } } PK{\U[C[Ctests/privacy/provider_test.phpnu[. namespace core_sms\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use core_sms\manager; /** * Tests for sms * * @package core_sms * @category test * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_sms\privacy\provider */ final class provider_test extends \core_privacy\tests\provider_testcase { public function test_get_metadata(): void { $collection = new collection('core_sms'); $newcollection = provider::get_metadata($collection); $this->assertCount(1, $newcollection->get_collection()); $types = $newcollection->get_collection(); $type = reset($types); $this->assertEquals('sms_messages', $type->get_name()); } public function test_get_contexts_for_userid_no_messagse(): void { $user = get_admin(); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertInstanceOf(contextlist::class, $contextlist); $this->assertCount(0, $contextlist); } public function test_get_contexts_for_userid_with_messages(): void { $this->resetAfterTest(true); $user = get_admin(); $manager = \core\di::get(manager::class); $message = $manager->send( recipientnumber: '12345', content: 'Hello world', component: 'core_sms', messagetype: 'example', recipientuserid: $user->id, async: false, ); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertInstanceOf(contextlist::class, $contextlist); $this->assertCount(1, $contextlist); $context = $contextlist->current(); $this->assertInstanceOf(\core\context\user::class, $context); $this->assertEquals($user->id, $context->instanceid); } public function test_export_user_data(): void { global $DB; $this->resetAfterTest(true); $clock = $this->mock_clock_with_frozen(99999); $user = get_admin(); $otheruser = $this->getDataGenerator()->create_user(); $manager = \core\di::get(manager::class); $manager->send( recipientnumber: '12345', content: 'Hello world', component: 'core_sms', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $otheruser->id, async: false, ); $clock->set_to(10000000); // Get the data for the User. $usercontext = \core\context\user::instance($user->id); /** @var \core_privacy\tests\request\content_writer */ $writer = writer::with_context($usercontext); $this->assertFalse($writer->has_any_data_in_any_context()); // Export data for the user. $this->export_all_data_for_user($user->id, 'core_sms'); $this->assertTrue($writer->has_any_data_in_any_context()); $data = $writer->get_data([get_string('sms', 'core_sms')]); $this->assertObjectHasProperty('messages', $data); $this->assertCount(2, $data->messages); $message = reset($data->messages); $this->assertArrayHasKey('content', $message); $this->assertEquals('Hello world', $message['content']); $this->assertArrayHasKey('messagetype', $message); $this->assertEquals('example', $message['messagetype']); $this->assertArrayHasKey('status', $message); $this->assertArrayHasKey('timecreated', $message); $this->assertEquals(99999, $message['timecreated']); } public function test_export_course_data(): void { $this->resetAfterTest(true); $clock = $this->mock_clock_with_frozen(99999); $course = $this->getDataGenerator()->create_course(); $user = get_admin(); $otheruser = $this->getDataGenerator()->create_user(); $manager = \core\di::get(manager::class); $manager->send( recipientnumber: '12345', content: 'Hello world', component: 'core_sms', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $otheruser->id, async: false, ); $clock->set_to(10000000); // Get the data for the User Context. $usercontext = \core\context\user::instance($user->id); /** @var \core_privacy\tests\request\content_writer */ $writer = writer::with_context($usercontext); $this->assertFalse($writer->has_any_data_in_any_context()); // Export data for the Course - there should be none. $this->export_context_data_for_user($user->id, \core\context\course::instance($course->id), 'core_sms'); $this->assertFalse($writer->has_any_data_in_any_context()); } public function test_delete_data_for_all_users_in_context(): void { global $DB; $this->resetAfterTest(true); $user = get_admin(); $otheruser = $this->getDataGenerator()->create_user(); $manager = \core\di::get(manager::class); $manager->send( recipientnumber: '12345', content: 'Hello world', component: 'core_sms', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $otheruser->id, async: false, ); $this->assertCount(2, $DB->get_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(3, $DB->count_records('sms_messages')); // No data is store under the system context. Nothing should be deleted. provider::delete_data_for_all_users_in_context(\core\context\system::instance()); $this->assertEquals(3, $DB->count_records('sms_messages')); $usercontext = \core\context\user::instance($user->id); provider::delete_data_for_all_users_in_context($usercontext); $this->assertEmpty($DB->get_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(1, $DB->count_records('sms_messages')); } public function test_delete_data_for_user(): void { global $DB; $this->resetAfterTest(true); $user = get_admin(); $otheruser = $this->getDataGenerator()->create_user(); $manager = \core\di::get(manager::class); $manager->send( recipientnumber: '12345', content: 'Hello world', component: 'core_sms', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $otheruser->id, async: false, ); $this->assertCount(2, $DB->get_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(3, $DB->count_records('sms_messages')); $usercontext = \core\context\user::instance($user->id); provider::delete_data_for_user(new approved_contextlist($user, 'core_sms', [$usercontext->id])); $this->assertEmpty($DB->get_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(1, $DB->count_records('sms_messages')); } public function test_delete_data_for_users_incorrect_userid(): void { global $DB; $this->resetAfterTest(true); $user = get_admin(); $otheruser = $this->getDataGenerator()->create_user(); $manager = \core\di::get(manager::class); $manager->send( recipientnumber: '12345', content: 'Hello world', component: 'core_sms', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $otheruser->id, async: false, ); $this->assertEquals(2, $DB->count_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(1, $DB->count_records('sms_messages', ['recipientuserid' => $otheruser->id])); $this->assertEquals(3, $DB->count_records('sms_messages')); $usercontext = \core\context\user::instance($user->id); // Data is all stored under the users own context. provider::delete_data_for_users(new approved_userlist( $usercontext, 'core_sms', [$otheruser->id] )); $this->assertEquals(0, $DB->count_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(1, $DB->count_records('sms_messages', ['recipientuserid' => $otheruser->id])); $this->assertEquals(1, $DB->count_records('sms_messages')); } public function test_delete_data_for_users_correct_user(): void { global $DB; $this->resetAfterTest(true); $user = get_admin(); $otheruser = $this->getDataGenerator()->create_user(); $manager = \core\di::get(manager::class); $manager->send( recipientnumber: '12345', content: 'Hello world', component: 'core_sms', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $otheruser->id, async: false, ); $this->assertEquals(2, $DB->count_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(1, $DB->count_records('sms_messages', ['recipientuserid' => $otheruser->id])); $this->assertEquals(3, $DB->count_records('sms_messages')); $usercontext = \core\context\user::instance($user->id); // Deleting data for both users only deletes that user's content. provider::delete_data_for_users(new approved_userlist( $usercontext, 'core_sms', [$user->id, $otheruser->id], )); $this->assertEquals(0, $DB->count_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(1, $DB->count_records('sms_messages', ['recipientuserid' => $otheruser->id])); $this->assertEquals(1, $DB->count_records('sms_messages')); } public function test_delete_data_for_users_wrong_context(): void { global $DB; $this->resetAfterTest(true); $user = get_admin(); $otheruser = $this->getDataGenerator()->create_user(); $manager = \core\di::get(manager::class); $manager->send( recipientnumber: '12345', content: 'Hello world', component: 'core_sms', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $otheruser->id, async: false, ); $this->assertEquals(2, $DB->count_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(1, $DB->count_records('sms_messages', ['recipientuserid' => $otheruser->id])); $this->assertEquals(3, $DB->count_records('sms_messages')); // Incorrect contexts are ignored. provider::delete_data_for_users(new approved_userlist( \core\context\system::instance(), 'core_sms', [$user->id, $otheruser->id], )); $this->assertEquals(2, $DB->count_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(1, $DB->count_records('sms_messages', ['recipientuserid' => $otheruser->id])); $this->assertEquals(3, $DB->count_records('sms_messages')); } public function test_get_users_in_context(): void { global $DB; $this->resetAfterTest(true); $user = get_admin(); $otheruser = $this->getDataGenerator()->create_user(); $manager = \core\di::get(manager::class); $manager->send( recipientnumber: '12345', content: 'Hello world', component: 'core_sms', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $user->id, async: false, ); $manager->send( recipientnumber: '12345', content: 'Hello, world!', component: 'core_course', messagetype: 'example', recipientuserid: $otheruser->id, async: false, ); $this->assertCount(2, $DB->get_records('sms_messages', ['recipientuserid' => $user->id])); $this->assertEquals(3, $DB->count_records('sms_messages')); // Get the users in the user context - should just be the user of that user context. $userlist = new userlist(\core\context\user::instance($user->id), 'core_sms'); provider::get_users_in_context($userlist); $this->assertEquals([$user->id], $userlist->get_userids()); // No users in the system context. $userlist = new userlist(\core\context\system::instance(), 'core_sms'); provider::get_users_in_context($userlist); $this->assertEquals([], $userlist->get_userids()); } } PK{\ˉ tests/gateway_test.phpnu[. namespace core_sms; /** * Tests for sms gateway. * * @package core_sms * @category test * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_sms\gateway */ final class gateway_test extends \advanced_testcase { public static function setUpBeforeClass(): void { require_once(__DIR__ . "/fixtures/dummy_gateway.php"); parent::setUpBeforeClass(); } public function test_update_message_status(): void { $this->resetAfterTest(); $manager = \core\di::get(\core_sms\manager::class); $config = new \stdClass(); $config->api_key = 'test_api_key'; $gw = $manager->create_gateway_instance( classname: \smsgateway_dummy\gateway::class, name: 'dummy', enabled: true, config: $config, ); $othergw = $manager->create_gateway_instance( classname: \smsgateway_dummy\gateway::class, name: 'dummy', enabled: true, config: $config, ); $message = new message( recipientnumber: '1234567890', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: false, gatewayid: $gw->id, ); $message2 = new message( recipientnumber: '1234567890', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: false, gatewayid: $gw->id, ); $updatedmessage = $gw->update_message_status($message); $this->assertEquals($message, $updatedmessage); $updatedmessages = $gw->update_message_statuses([$message, $message2]); $this->assertEquals([$message, $message2], $updatedmessages); $this->expectException(\coding_exception::class); $othergw->update_message_status($message); } } PK{\Od׫tests/message_status_test.phpnu[. namespace core_sms; /** * Tests for sms * * @package core_sms * @category test * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_sms\message_status * @covers \core_sms\status */ final class message_status_test extends \advanced_testcase { public function test_meta_status(): void { $sent = [ message_status::GATEWAY_SENT, ]; $failed = [ message_status::MESSAGE_OVER_SIZE, message_status::GATEWAY_FAILED, message_status::GATEWAY_NOT_AVAILABLE, message_status::GATEWAY_REJECTED, ]; $inprogress = [ message_status::GATEWAY_QUEUED, ]; foreach (message_status::cases() as $case) { $this->assertEquals(in_array($case, $sent), $case->is_sent()); $this->assertEquals(in_array($case, $failed), $case->is_failed()); $this->assertEquals(in_array($case, $inprogress), $case->is_in_progress()); } } public function test_description(): void { foreach (message_status::cases() as $case) { $this->assertInstanceOf(\lang_string::class, $case->description()); } } } PK{\~U)tests/message_test.phpnu[. namespace core_sms; use ValueError; /** * Tests for SMS Messages. * * @package core_sms * @category test * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_sms\message */ final class message_test extends \advanced_testcase { public function test_create(): void { $message = new message( recipientnumber: '1234567890', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: false, ); $this->assertInstanceOf(message::class, $message); $this->assertFalse($message->is_sent()); } public function test_timecreated(): void { $clock = $this->mock_clock_with_incrementing(55555); $timecreated = 12345; $message = new message( recipientnumber: '1234567890', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: false, timecreated: $timecreated, ); $this->assertEquals($timecreated, $message->timecreated); $starttime = $clock->now(); $message = new message( recipientnumber: '1234567890', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: false, ); $this->assertGreaterThan($starttime->getTimestamp(), $message->timecreated); $this->assertLessThan($clock->now()->getTimestamp(), $message->timecreated); } public function test_id_not_updatable(): void { $message = new message( recipientnumber: '1234567890', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: false, ); $this->assertFalse(isset($message->id)); $message = $message->with(id: 123); $this->assertEquals(123, $message->id); $this->expectException(\coding_exception::class); $message->with(id: 987); } public function test_get_region_invalid(): void { $message = new message( recipientnumber: '1234567890', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: false, ); $this->expectException(ValueError::class); $message->get_region(); } /** * Test that get_region returns regions with valid numbers. * * @dataProvider get_region_provider * @param string $recipientnumber * @param string $expectedregion */ public function test_get_region_valid( string $recipientnumber, string $expectedregion, ): void { $message = new message( recipientnumber: $recipientnumber, content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: false, ); $this->assertEquals($expectedregion, $message->get_region()); } /** * Data provider for test_get_region_valid. * * @return array */ public static function get_region_provider(): array { return [ // Authorised fictional numbers only. // Australia: https://www.acma.gov.au/phone-numbers-use-tv-shows-films-and-creative-works. ['+61491570006', 'AU'], // UK: https://www.ofcom.org.uk/phones-telecoms-and-internet/information-for-industry/numbering/numbers-for-drama. ['+447400123456', 'GB'], ]; } } PK{\%tests/behat/list_sms_gateways.featurenu[@sms Feature: Access the SMS gateways page In order to maintain SMS functionality As an admin I need to be able to view and manage SMS gateways Background: Manage the SMS gateways Given the following "core_sms > sms_gateways" exist: | name | classname | enabled | config | | Default | smsgateway_aws\gateway | 1 | {"countrycode":"8802","gateway":"aws_sns"} | | Europe | smsgateway_aws\gateway | 1 | {"countrycode":"+61","gateway":"aws_sns"} | @javascript Scenario: An admin can manage the SMS gateways in the gateway management page Given I log in as "admin" And I navigate to "Plugins > SMS > Manage SMS gateways" in site administration And I should see "SMS gateways" And I should see "Create new SMS gateway" And I should see "Default" in the "sms_gateways_table" "table" And I should see "Europe" in the "sms_gateways_table" "table" And I should see "Europe" in the "sms_gateways_table" "table" And I should see "Disable Default" in the "Default" "table_row" And I should see "Disable Europe" in the "Europe" "table_row" When I toggle the "Disable Default" admin switch "off" Then I should see "Default disabled" And I should see "Enable Default" in the "Default" "table_row" And I should see "Edit" in the "Default" "table_row" And I should see "Delete" in the "Default" "table_row" And I click on "Delete" "link" in the "Default" "table_row" And I should see "This will delete the Default SMS gateway." And I click on "Delete" "button" And I should see "Default SMS gateway deleted" And I should not see "Default" in the "sms_gateways_table" "table" PK{\.uJJtests/manager_test.phpnu[. namespace core_sms; use core_sms\task\send_sms_task; /** * Tests for sms manager * * @package core_sms * @category test * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_sms\manager * @covers \core_sms\message * @covers \core_sms\gateway */ final class manager_test extends \advanced_testcase { public static function setUpBeforeClass(): void { require_once(__DIR__ . "/fixtures/dummy_gateway.php"); parent::setUpBeforeClass(); } public function test_gateway_manipulation(): void { $this->resetAfterTest(); $config = (object) [ 'data' => 'goeshere', ]; $dummy = $this->getMockBuilder(\core_sms\gateway::class) ->setConstructorArgs([ 'enabled' => true, 'name' => 'dummy', 'config' => json_encode($config), ]) ->onlyMethods(['get_send_priority', 'send']) ->getMock(); $dummygw = get_class($dummy); $manager = \core\di::get(\core_sms\manager::class); $gateway = $manager->create_gateway_instance( classname: $dummygw, name: 'dummy', enabled: true, config: $config, ); $this->assertIsInt($gateway->id); $this->assertTrue($gateway->enabled); $this->assertEquals('goeshere', $gateway->config->data); // Disable the gateway. $disabled = $manager->disable_gateway($gateway); $this->assertFalse($disabled->enabled); $this->assertEquals($gateway->id, $disabled->id); $this->assertEquals($gateway->config, $disabled->config); $this->assertTrue($gateway->enabled); // Enable the gateway. $enabled = $manager->enable_gateway($disabled); $this->assertTrue($enabled->enabled); $this->assertEquals($disabled->id, $enabled->id); $this->assertEquals($gateway->config, $enabled->config); $this->assertFalse($disabled->enabled); // Enabling an enabled gateway should return an identical object. // Note: Whether the object is identical is not guaranteed, and is internal logic we should not be concerned with. $reenabled = $manager->enable_gateway($enabled); $this->assertEquals($enabled, $reenabled); } public function test_create_gateway_instance_unknown_class(): void { $manager = \core\di::get(\core_sms\manager::class); $this->expectException(\coding_exception::class); $manager->create_gateway_instance( classname: \no\class\name\here::class, name: 'dummy', enabled: true, config: (object) [ 'data' => 'goeshere', ], ); } public function test_create_gateway_instance_valid_but_wrong_class(): void { $manager = \core\di::get(\core_sms\manager::class); $this->expectException(\coding_exception::class); $manager->create_gateway_instance( classname: self::class, name: 'dummy', enabled: true, config: (object) [ 'data' => 'goeshere', ], ); } /** * Test that uninstalled gateways do not cause failures in the workflow. */ public function test_uninstalled_gateway(): void { // We should prevent removal of gateways which hold any data, but if one has been removed, we should not fail. $this->resetAfterTest(); $config = (object) [ 'data' => 'goeshere', ]; $dummy = $this->getMockBuilder(\core_sms\gateway::class) ->setConstructorArgs([ 'enabled' => true, 'name' => 'dummy', 'config' => json_encode($config), ]) ->onlyMethods(['get_send_priority', 'send']) ->getMock(); $dummygw = get_class($dummy); $manager = \core\di::get(\core_sms\manager::class); $gateway = $manager->create_gateway_instance( classname: $dummygw, name: 'dummy', enabled: true, config: $config, ); $uninstalledgateway = $manager->create_gateway_instance( classname: $dummygw, name: 'dummy', enabled: true, config: $config, ); $db = \core\di::get(\moodle_database::class); $db->set_field('sms_gateways', 'gateway', 'uninstalled', ['id' => $uninstalledgateway->id]); $instances = $manager->get_gateway_instances(); $this->assertDebuggingCalled(); $this->assertCount(1, $instances); $this->assertArrayHasKey($gateway->id, $instances); $this->assertArrayNotHasKey($uninstalledgateway->id, $instances); } /** * Test that multiple instances of the same gateway can be created. */ public function test_multiple_gateway_instances(): void { $this->resetAfterTest(); $config = (object) [ 'data' => 'goeshere', ]; $dummy = $this->getMockBuilder(\core_sms\gateway::class) ->setConstructorArgs([ 'enabled' => true, 'name' => 'dummy', 'config' => json_encode($config), ]) ->onlyMethods(['get_send_priority', 'send']) ->setMockClassName('dummygateway') ->getMock(); $dummygw = get_class($dummy); $otherdummy = $this->getMockBuilder(\core_sms\gateway::class) ->setConstructorArgs([ 'enabled' => true, 'name' => 'dummy', 'config' => json_encode($config), ]) ->onlyMethods(['get_send_priority', 'send']) ->setMockClassName('otherdummygw') ->getMock(); $otherdummygw = get_class($otherdummy); $manager = \core\di::get(\core_sms\manager::class); $gatewaya = $manager->create_gateway_instance( classname: $dummygw, name: 'dummy', enabled: true, config: $config, ); $gatewayb = $manager->create_gateway_instance( classname: $otherdummygw, name: 'dummy', enabled: true, config: $config, ); $gatewayc = $manager->create_gateway_instance( classname: $dummygw, name: 'dummy', config: $config, ); $this->assertNotEquals($gatewaya->id, $gatewayb->id); $this->assertNotEquals($gatewaya->id, $gatewayc->id); $this->assertNotEquals($gatewayb->id, $gatewayc->id); $instances = $manager->get_gateway_instances(); $this->assertCount(3, $instances); $this->assertArrayHasKey($gatewaya->id, $instances); $this->assertArrayHasKey($gatewayb->id, $instances); $this->assertArrayHasKey($gatewayc->id, $instances); $enabled = $manager->get_enabled_gateway_instances(); $this->assertCount(2, $enabled); $this->assertArrayHasKey($gatewaya->id, $enabled); $this->assertArrayHasKey($gatewayb->id, $enabled); $this->assertArrayNotHasKey($gatewayc->id, $enabled); $dummygwinstances = $manager->get_gateway_instances(['gateway' => $dummygw]); $this->assertCount(2, $dummygwinstances); $this->assertArrayHasKey($gatewaya->id, $dummygwinstances); $this->assertArrayNotHasKey($gatewayb->id, $dummygwinstances); $this->assertArrayHasKey($gatewayc->id, $dummygwinstances); } /** * Test that the manager can get gateways for a message. * * @dataProvider gateway_priority_provider * @param string $recipientnumber * @param int $matchcount * @param ?string $gw */ public function test_get_gateways_for_message( string $recipientnumber, int $matchcount, ?string $gw, ): void { $this->resetAfterTest(); $manager = \core\di::get(\core_sms\manager::class); $ukgw = $manager->create_gateway_instance(\smsgateway_dummy\gateway::class, 'dummy', true, (object) [ 'startswith' => (object) [ '+44' => 100, '+61' => 1, ], 'priority' => 0, ]); $augw = $manager->create_gateway_instance(\smsgateway_dummy\gateway::class, 'dummy', true, (object) [ 'startswith' => (object) [ '+44' => 1, '+61' => 100, ], 'priority' => 0, ]); $message = new message( recipientnumber: $recipientnumber, content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: false, ); $gateways = $manager->get_possible_gateways_for_message($message); $this->assertCount($matchcount, $gateways); $preferredgw = $manager->get_gateway_for_message($message); if ($gw === null) { $this->assertNull($preferredgw); $this->assertFalse($ukgw->can_send($message)); $this->assertFalse($augw->can_send($message)); } else { $this->assertEquals(${$gw}->id, $preferredgw->id); $this->assertTrue(${$gw}->can_send($message)); } } /** * Data provider for test_get_gateways_for_message tests. * * @return array */ public static function gateway_priority_provider(): array { return [ 'uk' => [ '+447123456789', 2, 'ukgw', ], 'au' => [ '+61987654321', 2, 'augw', ], 'us' => [ '+1987654321', 0, null, ], ]; } public function test_save_message(): void { $this->resetAfterTest(); $manager = \core\di::get(\core_sms\manager::class); $message = new message( recipientnumber: '+447123456789', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: false, ); $saved = $manager->save_message($message); $this->assertFalse(isset($message->id)); $this->assertTrue(isset($saved->id)); $storedmessage = $manager->get_message(['id' => $saved->id]); $this->assertEquals($saved, $storedmessage); $updatedmessage = $manager->save_message($saved->with(status: message_status::GATEWAY_SENT)); $this->assertEquals($saved->id, $updatedmessage->id); $this->assertEquals(message_status::GATEWAY_SENT, $updatedmessage->status); $this->assertEquals($saved->recipientnumber, $updatedmessage->recipientnumber); $this->assertEquals($saved->content, $updatedmessage->content); $this->assertEquals($saved->component, $updatedmessage->component); $this->assertEquals($saved->messagetype, $updatedmessage->messagetype); $this->assertEquals($saved->recipientuserid, $updatedmessage->recipientuserid); $this->assertEquals($saved->issensitive, $updatedmessage->issensitive); } public function test_send(): void { $this->resetAfterTest(); $config = new \stdClass(); $config->priority = 50; $manager = \core\di::get(\core_sms\manager::class); $gw = $manager->create_gateway_instance( classname: \smsgateway_dummy\gateway::class, name: 'dummy', enabled: true, config: $config, ); $message = $manager->send( recipientnumber: '+447123456789', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, async: false, ); $this->assertInstanceOf(message::class, $message); $this->assertIsInt($message->id); $this->assertEquals(message_status::GATEWAY_SENT, $message->status); $this->assertEquals($gw->id, $message->gatewayid); $this->assertEquals('Hello, world!', $message->content); $storedmessage = $manager->get_message(['id' => $message->id]); $this->assertEquals($message, $storedmessage); } public function test_send_issensitive(): void { $this->resetAfterTest(); $manager = \core\di::get(\core_sms\manager::class); $config = new \stdClass(); $config->priority = 50; $gw = $manager->create_gateway_instance(\smsgateway_dummy\gateway::class, 'dummy', true, $config); $message = $manager->send( recipientnumber: '+447123456789', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: true, async: false, ); $this->assertInstanceOf(message::class, $message); $this->assertIsInt($message->id); $this->assertEquals(message_status::GATEWAY_SENT, $message->status); $this->assertEquals($gw->id, $message->gatewayid); $this->assertNull($message->content); $storedmessage = $manager->get_message(['id' => $message->id]); $this->assertEquals($message, $storedmessage); } public function test_send_issensitive_async(): void { $this->resetAfterTest(); $manager = \core\di::get(\core_sms\manager::class); $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Sensitive messages cannot be sent asynchronously'); $manager->send( recipientnumber: '+447123456789', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, issensitive: true, async: true, ); } /** * Test sending SMS asynchronously. */ public function test_send_async(): void { $this->resetAfterTest(); $config = new \stdClass(); $config->priority = 50; $manager = \core\di::get(\core_sms\manager::class); $manager->create_gateway_instance( classname: \smsgateway_dummy\gateway::class, name: 'dummy', enabled: true, config: $config, ); $message = $manager->send( recipientnumber: '+447123456789', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, ); $this->assertInstanceOf(message::class, $message); $this->assertIsInt($message->id); $this->assertEquals(message_status::GATEWAY_QUEUED, $message->status); $this->assertEquals('Hello, world!', $message->content); $messagedbrecords = $manager->get_messages(); $this->assertInstanceOf(\Generator::class, $messagedbrecords); $messages = iterator_to_array($messagedbrecords); $this->assertCount(1, $messages); $storedmessage = $manager->get_message(['id' => $message->id]); $this->assertEquals($message, $storedmessage); $adhoctask = \core\task\manager::get_adhoc_tasks(send_sms_task::class); $this->assertCount(1, $adhoctask); // Now lets run the task and check if SMS is sent. $this->run_all_adhoc_tasks(); $message = $manager->get_message(['id' => $message->id]); $this->assertEquals(message_status::GATEWAY_SENT, $message->status); } public function test_send_no_gateway(): void { $this->resetAfterTest(); $manager = \core\di::get(\core_sms\manager::class); $message = $manager->send( recipientnumber: '+447123456789', content: 'Hello, world!', component: 'core', messagetype: 'test', recipientuserid: null, async: false, ); $this->assertInstanceOf(message::class, $message); $this->assertIsInt($message->id); $this->assertEquals(message_status::GATEWAY_NOT_AVAILABLE, $message->status); $this->assertEmpty($message->gatewayid); } /** * Test the truncate content process while sending the SMS. */ public function test_send_truncate_content(): void { $this->resetAfterTest(); $config = new \stdClass(); $config->priority = 50; $manager = \core\di::get(\core_sms\manager::class); $manager->create_gateway_instance( classname: \smsgateway_dummy\gateway::class, name: 'dummy', enabled: true, config: $config, ); $message = $manager->send( recipientnumber: '+447123456789', content: str_repeat('a', 161), component: 'core', messagetype: 'test', recipientuserid: null, async: false, ); // Expected the message to be truncated with the length limit. $this->assertEquals( expected: str_repeat('a', 160), // 160 is the limit of the dummy gateway. actual: $message->content, ); } public function test_get_messages(): void { $db = $this->createStub(\moodle_database::class); $db->method('get_records')->willReturn([ (object) [ 'id' => 1, 'recipientnumber' => '+447123456789', 'content' => 'Hello, world!', 'component' => 'core', 'messagetype' => 'test', 'recipientuserid' => null, 'issensitive' => false, 'status' => message_status::GATEWAY_SENT->value, 'gatewayid' => 1, 'timecreated' => time(), ], ]); \core\di::set(\moodle_database::class, $db); $manager = \core\di::get(\core_sms\manager::class); $result = $manager->get_messages(); $this->assertInstanceOf(\Generator::class, $result); $messages = iterator_to_array($result); $this->assertCount(1, $messages); array_walk($messages, fn ($message) => $this->assertInstanceOf(message::class, $message)); } } PK{\5Օ configure.phpnu[. /** * Configure sms gateway for a given instance. * * @package core_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_login(); $context = context_system::instance(); require_capability('moodle/site:config', $context); $id = optional_param('id', null, PARAM_INT); $gateway = optional_param('smsgateway', null, PARAM_PLUGIN); $returnurl = optional_param('returnurl', null, PARAM_LOCALURL); $title = get_string('createnewgateway', 'sms'); $data = []; $urlparams = []; if ($id) { $urlparams['id'] = $id; } if ($gateway) { $urlparams['gateway'] = $gateway; } if (!empty($gateway)) { $configs = new stdClass(); $configs->smsgateway = $gateway; $data = [ 'gatewayconfigs' => $configs, ]; } if (!empty($id)) { $manager = \core\di::get(\core_sms\manager::class); $gatewayrecord = $manager->get_gateway_records(['id' => $id]); $gatewayrecord = reset($gatewayrecord); $plugin = explode('\\', $gatewayrecord->gateway); $plugin = $plugin[0]; $configs = json_decode($gatewayrecord->config, true, 512, JSON_THROW_ON_ERROR); $configs = (object) $configs; $configs->smsgateway = $plugin; $configs->id = $gatewayrecord->id; $configs->name = $gatewayrecord->name; $data = [ 'gatewayconfigs' => $configs, ]; $a = ['gateway' => $gatewayrecord->name]; $title = get_string('edit_sms_gateway', 'sms', $a); } $PAGE->set_context($context); $PAGE->set_url('/sms/configure.php', $urlparams); $PAGE->set_title($title); $PAGE->set_heading($title); if (empty($returnurl)) { $returnurl = new moodle_url('/sms/sms_gateways.php'); } else { $returnurl = new moodle_url($returnurl); } $data['returnurl'] = $returnurl; $mform = new \core_sms\form\sms_gateway_form(customdata: $data); if ($mform->is_cancelled()) { $data = $mform->get_data(); if (isset($data->returnurl)) { redirect($data->returnurl); } else { redirect($returnurl); } } if ($data = $mform->get_data()) { $manager = \core\di::get(\core_sms\manager::class); $smsgateway = $data->smsgateway; $gatewayname = $data->name; // The $data will go into the database config column. If any data is not needed, unset it here. unset($data->smsgateway, $data->name, $data->id, $data->saveandreturn, $data->returnurl); if (!empty($id)) { $gatewayinstance = $manager->get_gateway_instances(['id' => $id]); $gatewayinstance = reset($gatewayinstance); $gatewayinstance->name = $gatewayname; $manager->update_gateway_instance($gatewayinstance, $data); } else { $classname = $smsgateway . '\\' . 'gateway'; $manager->create_gateway_instance( classname: $classname, name: $gatewayname, enabled: true, config: $data, ); } redirect($returnurl); } echo $OUTPUT->header(); $mform->display(); echo $OUTPUT->footer(); PKR\mQjqqdb/install.phpnu[. /** * Installation code for the SMS message processor. * * @package message_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Add the records for SMS message processor. * * @return bool */ function xmldb_message_sms_install(): bool { // Insert the processor record for sms. global $DB; $provider = new stdClass(); $provider->name = 'sms'; $DB->insert_record('message_processors', $provider); // Keep the plugin disabled by default. $class = \core_plugin_manager::resolve_plugininfo_class('message'); $class::enable_plugin($provider->name, 0); return true; } PKR\lang/en/message_sms.phpnu[. /** * Strings for component 'message_sms', language 'en'. * * @package message_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'SMS'; $string['privacy:metadata'] = 'The SMS processor does not store any personal data.'; PKR\YG version.phpnu[. /** * SMS notification processor version information. * * @package message_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'message_sms'; // Full name of the plugin (used for diagnostics). PKR\]/kk!tests/message_output_sms_test.phpnu[. namespace message_sms; use core_sms\message; use core_sms\message_status; use core_sms\task\send_sms_task; use mod_assign\notification_helper; /** * SMS processor tests. * * @package message_sms * @copyright 2025 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \message_output_sms */ final class message_output_sms_test extends \advanced_testcase { /** * Test the SMS output for the SMS processor. */ public function test_sms_output(): void { global $CFG; require_once($CFG->dirroot . "/sms/tests/fixtures/dummy_gateway.php"); $this->preventResetByRollback(); $this->resetAfterTest(); $config = new \stdClass(); $config->priority = 50; $manager = \core\di::get(\core_sms\manager::class); $manager->create_gateway_instance( classname: \smsgateway_dummy\gateway::class, name: 'dummy', enabled: true, config: $config, ); // Set the configs and enable SMS. \core\plugininfo\message::enable_plugin( pluginname: 'sms', enabled: 1, ); // We have to use assign because this is the only component supports sms at the moment. set_config('message_provider_mod_assign_assign_due_digest_enabled', 'sms', 'message'); $user = self::getDataGenerator()->create_user(); $user->phone2 = '+61000000000'; $user2 = self::getDataGenerator()->create_user(); $CFG->noreplyuser = $user2->id; // Send a notification. $message = new \core\message\message(); $message->component = 'mod_assign'; $message->name = notification_helper::TYPE_DUE_DIGEST; $message->userfrom = \core_user::get_noreply_user(); $message->userto = $user; $message->subject = 'Hello'; $message->fullmessageformat = FORMAT_PLAIN; $message->fullmessage = 'Hello message'; $message->fullmessagehtml = 'Hello message'; $message->smallmessage = 'Hello message'; $message->fullmessagesms = 'Hello sms message'; $message->notification = 1; message_send($message); $messagedbrecords = $manager->get_messages(); $this->assertInstanceOf(\Generator::class, $messagedbrecords); $messages = iterator_to_array($messagedbrecords); $this->assertCount(1, $messages); $message = $messages[0]; $this->assertInstanceOf(message::class, $message); $this->assertIsInt($message->id); $this->assertEquals(message_status::GATEWAY_QUEUED, $message->status); $this->assertEquals('Hello sms message', $message->content); $storedmessage = $manager->get_message(['id' => $message->id]); $this->assertEquals($message, $storedmessage); $adhoctask = \core\task\manager::get_adhoc_tasks(send_sms_task::class); $this->assertCount(1, $adhoctask); // Now lets run the task and check if SMS is sent. $this->run_all_adhoc_tasks(); $message = $manager->get_message(['id' => $message->id]); $this->assertEquals(message_status::GATEWAY_SENT, $message->status); } } PKR\tu8message_output_sms.phpnu[. defined('MOODLE_INTERNAL') || die(); use core_sms\message_status; require_once($CFG->dirroot . '/message/output/lib.php'); require_once($CFG->libdir . '/moodlelib.php'); /** * Message processor for SMS. * * @package message_sms * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class message_output_sms extends message_output { #[\Override] public function send_message($message): bool { $userdata = is_object($message->userto) ? $message->userto : get_complete_user_data('id', $message->userto); if (empty($userdata) || !$this->is_user_configured($userdata) || !$this->should_send_sms($message)) { return false; } $manager = \core\di::get(\core_sms\manager::class); $result = $manager->send( recipientnumber: $userdata->phone2, content: $message->fullmessagesms, component: $message->component, messagetype: $message->name, recipientuserid: $userdata->id, ); if ($result->status === message_status::GATEWAY_QUEUED || $result->status === message_status::GATEWAY_SENT ) { return true; } debugging($result->status->description()); return false; } #[\Override] public function is_user_configured($user = null): bool { if (empty($user)) { return false; } // Skip any SMS if user doesn't have a mobile number. if (empty($user->phone2)) { mtrace('No mobile number found for userid: ' . $user->id); return false; } // Skip any messaging of suspended and deleted users. if ( $user->auth === 'nologin' || $user->suspended || $user->deleted ) { mtrace('The user with userid: ' . $user->id . ' is either deleted or suspended. Can not send SMS.'); return false; } return true; } /** * Check whether the SMS message data fulfills the requirements. * * @param stdclass $messagedata The message data * @return bool */ public function should_send_sms(stdclass $messagedata): bool { // Don't send SMS if it's not a production site and the following config is set. if (!empty($CFG->nosmsever)) { mtrace('Can not send SMS while nosmsever is enabled.'); return false; } // We don't have a fallback for SMS text. It has to be included. if (!isset($messagedata->fullmessagesms)) { mtrace('No SMS string found for the message'); return false; } // Check support for SMS from the component. if (!core_message\helper::supports_sms_notifications($messagedata)) { return false; } return true; } #[\Override] public function load_data(&$preferences, $userid) { return; } #[\Override] public function config_form($preferences) { return; } #[\Override] public function process_form($form, &$preferences) { return; } #[\Override] public function get_default_messaging_settings() { return MESSAGE_DISALLOWED; } #[\Override] public function can_send_to_any_users() { return true; } } PK{\Sz7gateway/aws/db/hooks.phpnu[PK{\ w&gateway/aws/lang/en/smsgateway_aws.phpnu[PK{\~ ;; gateway/aws/version.phpnu[PK{\/#(7gateway/aws/classes/privacy/provider.phpnu[PK{\4:  64gateway/aws/classes/local/aws_sms_service_provider.phpnu[PK{\M7 -gateway/aws/classes/local/service/aws_sns.phpnu[PK{\npy %&gateway/aws/classes/hook_listener.phpnu[PK{\bplMM5gateway/aws/classes/helper.phpnu[PK{\ƞ <gateway/aws/classes/gateway.phpnu[PK{\&m ! Fgateway/aws/tests/helper_test.phpnu[PK{\`$ "YTgateway/aws/tests/gateway_test.phpnu[PK{\#%{^gateway/modica/db/hooks.phpnu[PK{\"Ə<<,Ucgateway/modica/lang/en/smsgateway_modica.phpnu[PK{\DDhgateway/modica/version.phpnu[PK{\Z&+{mgateway/modica/classes/privacy/provider.phpnu[PK{\Nx (rgateway/modica/classes/hook_listener.phpnu[PK{\ R R "jgateway/modica/classes/gateway.phpnu[PK{\2ޕ% % %gateway/modica/tests/gateway_test.phpnu[PK{\*? T classes/message_status.phpnu[PK{\,;;!Zclasses/form/sms_gateway_form.phpnu[PK{\"Nuu'classes/external/sms_gateway_status.phpnu[PK{\ldXXclasses/privacy/provider.phpnu[PK{\77Vclasses/manager.phpnu[PK{\,'{classes/hook/before_gateway_deleted.phpnu[PK{\ (classes/hook/before_gateway_disabled.phpnu[PK{\E, classes/hook/after_sms_gateway_form_hook.phpnu[PK{\nwMclasses/message.phpnu[PK{\7"!,classes/description.phpnu[PK{\+SSA0classes/task/send_sms_task.phpnu[PK{\Zi**#7classes/table/sms_gateway_table.phpnu[PK{\8-_Wclasses/table/sms_gateway_table_filterset.phpnu[PK{\y[classes/gateway.phpnu[PK{\]쬑mclasses/status.phpnu[PK{\ϳ[[#ttemplates/sms_action_icons.mustachenu[PK{\=+.{templates/sms_gateways.mustachenu[PK{\ڄtamd/src/smsgatewaychooser.jsnu[PK{\!  &amd/build/smsgatewaychooser.min.js.mapnu[PK{\$w6"amd/build/smsgatewaychooser.min.jsnu[PK{\C  UPGRADING.mdnu[PK{\~- sms_gateways.phpnu[PK{\Lvv tests/fixtures/dummy_gateway.phpnu[PK{\r%$$tests/generator/lib.phpnu[PK{\lT,Ktests/generator/behat_core_sms_generator.phpnu[PK{\& *[tests/external/sms_gateway_status_test.phpnu[PK{\U[C[Cctests/privacy/provider_test.phpnu[PK{\ˉ  tests/gateway_test.phpnu[PK{\Od׫tests/message_status_test.phpnu[PK{\~U)tests/message_test.phpnu[PK{\%&,tests/behat/list_sms_gateways.featurenu[PK{\.uJJJ3tests/manager_test.phpnu[PK{\5Օ _~configure.phpnu[PKR\mQjqq1db/install.phpnu[PKR\lang/en/message_sms.phpnu[PKR\YG .version.phpnu[PKR\]/kk!tests/message_output_sms_test.phpnu[PKR\tu8իmessage_output_sms.phpnu[PK88+