diff --git a/sms/classes/manager.php b/sms/classes/manager.php index 35eb4b39479..8fae2544d44 100644 --- a/sms/classes/manager.php +++ b/sms/classes/manager.php @@ -315,4 +315,33 @@ class manager { 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; + } } diff --git a/sms/gateway/.placeholder b/sms/gateway/.placeholder deleted file mode 100644 index ac40f078387..00000000000 --- a/sms/gateway/.placeholder +++ /dev/null @@ -1 +0,0 @@ -This file should be removed when a gateway is added to core. diff --git a/sms/gateway/aws/classes/gateway.php b/sms/gateway/aws/classes/gateway.php new file mode 100644 index 00000000000..9bfdbfec077 --- /dev/null +++ b/sms/gateway/aws/classes/gateway.php @@ -0,0 +1,71 @@ +. + +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; + } +} diff --git a/sms/gateway/aws/classes/helper.php b/sms/gateway/aws/classes/helper.php new file mode 100644 index 00000000000..c90e171c6fc --- /dev/null +++ b/sms/gateway/aws/classes/helper.php @@ -0,0 +1,28 @@ +. + +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 { + +} diff --git a/sms/gateway/aws/classes/local/aws_sms_service_provider.php b/sms/gateway/aws/classes/local/aws_sms_service_provider.php new file mode 100644 index 00000000000..39e4fec3a15 --- /dev/null +++ b/sms/gateway/aws/classes/local/aws_sms_service_provider.php @@ -0,0 +1,43 @@ +. + +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; +} diff --git a/sms/gateway/aws/classes/local/service/aws_sns.php b/sms/gateway/aws/classes/local/service/aws_sns.php new file mode 100644 index 00000000000..c4767f5b595 --- /dev/null +++ b/sms/gateway/aws/classes/local/service/aws_sns.php @@ -0,0 +1,90 @@ +. + +namespace smsgateway_aws\local\service; + +use core\aws\aws_helper; +use core_sms\message_status; +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' => aws_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; + } + } +} diff --git a/sms/gateway/aws/classes/privacy/provider.php b/sms/gateway/aws/classes/privacy/provider.php new file mode 100644 index 00000000000..531a3713495 --- /dev/null +++ b/sms/gateway/aws/classes/privacy/provider.php @@ -0,0 +1,35 @@ +. + +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'; + } +} diff --git a/sms/gateway/aws/lang/en/smsgateway_aws.php b/sms/gateway/aws/lang/en/smsgateway_aws.php new file mode 100644 index 00000000000..f0dcce3eaab --- /dev/null +++ b/sms/gateway/aws/lang/en/smsgateway_aws.php @@ -0,0 +1,42 @@ +. + +/** + * 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'] = 'Key'; +$string['api_key_help'] = 'Amazon API key credential.'; +$string['api_region'] = 'Region'; +$string['api_region_help'] = 'Amazon API gateway region.'; +$string['api_secret'] = 'Secret'; +$string['api_secret_help'] = 'Amazon API secret credential.'; +$string['aws_sns'] = 'AWS SNS'; +$string['countrycode'] = 'Country number code'; +$string['countrycode_help'] = 'The calling code without the leading + as a default if users do not enter an international number with a + prefix. + +See this link for a list of calling codes: {$a}'; +$string['gateway'] = 'SMS Gateway'; +$string['gateway_help'] = 'The SMS provider you wish to send messages via'; +$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 credential provider chain'; + + diff --git a/sms/gateway/aws/tests/gateway_test.php b/sms/gateway/aws/tests/gateway_test.php new file mode 100644 index 00000000000..c9936193539 --- /dev/null +++ b/sms/gateway/aws/tests/gateway_test.php @@ -0,0 +1,64 @@ +. + +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 + */ +class gateway_test extends \advanced_testcase { + + public function test_update_message_status(): void { + $this->resetAfterTest(); + + $manager = \core\di::get(\core_sms\manager::class); + $gw = $manager->create_gateway_instance(gateway::class, true); + $othergw = $manager->create_gateway_instance(gateway::class, true); + + $message = new message( + recipientnumber: '1234567890', + content: 'Hello, world!', + component: 'smsgateway_aws', + messagetype: 'test', + recipientuserid: null, + sensitive: false, + gatewayid: $gw->id, + ); + $message2 = new message( + recipientnumber: '1234567890', + content: 'Hello, world!', + component: 'smsgateway_aws', + messagetype: 'test', + recipientuserid: null, + sensitive: 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); + } +} diff --git a/sms/gateway/aws/tests/helper_test.php b/sms/gateway/aws/tests/helper_test.php new file mode 100644 index 00000000000..07a3dc361a0 --- /dev/null +++ b/sms/gateway/aws/tests/helper_test.php @@ -0,0 +1,79 @@ +. + +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 + */ +class helper_test extends \advanced_testcase { + + /** + * Data provider for test_format_number(). + * + * @return array of different country codes and phone numbers. + */ + public 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)); + } +} diff --git a/sms/gateway/aws/version.php b/sms/gateway/aws/version.php new file mode 100644 index 00000000000..1f2f7ead273 --- /dev/null +++ b/sms/gateway/aws/version.php @@ -0,0 +1,30 @@ +. + +/** + * 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 = 2024042200; +$plugin->requires = 2024041600; +$plugin->maturity = MATURITY_STABLE;