From 0efe86600160b4dd14fac2db40aa55a828fd2f4b Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Thu, 17 Sep 2015 13:57:11 +0300 Subject: [PATCH] Bug 14767: SMS delivery notes from SMS Gateway provider's report Two-phase method to receive delivery notes from SMS Gateway provider. This is an example how to receive Gateway reports and most likely does not apply for your local provider. However, it should give you a good starting point to start receiving SMS delivery notes with REST API with your own provider. Example driver is SMS::Send::Labyrintti::Driver Phase ONE (POST to provider) -------------------------------- In phase ONE, as normally, we make a HTTP POST to our SMS Gateway provider and give them the details of SMS. They will instantly respond if the message was placed for delivery. If not, there must have been some error and we will throw a Koha::Exception::SMSDeliveryFailure. This will be caught by C4::Letters via C4::SMS. Message in the queue will be set as "failed" and the reason for failure will be added to delivery_note accordingly. If the delivery was succesful, we set the message as "sent" like before and continue to phase TWO. Phase TWO (provider to REST API) -------------------------------- In phase TWO, the message has been placed for delivery at our SMS Gateway provider. When the message has been processed and sent at their end, they will send us a POST request. We have added this into our Swagger definitions. If you want to limit the REST endpoint only to your provider, please see koha-httpd.conf where we will allow this endpoint to only certain IPs and block the rest. If the report says the message delivery was successful, we don't change anything. However, if the report says that the message delivery was failed, we will change the message status from "sent" to "failed" and add the reason of failure into delivery_note. Includes REST tests. Endpoints introduced in this patch: POST /api/v1/messages/{messagenumber}/report/labyrintti (for receiving the report) --- C4/Installer/PerlDependencies.pm | 5 ++ C4/Letters.pm | 6 ++ C4/SMS.pm | 1 + Koha/Exception/SMSDeliveryFailure.pm | 28 ++++++++ Koha/REST/V1/Messages/Reports.pm | 37 ++++++++++ SMS/Send/Labyrintti/Driver.pm | 129 +++++++++++++++++++++++++++++++++++ api/v1/swagger.json | 50 ++++++++++++++ etc/koha-conf.xml | 14 ++++ etc/koha-httpd.conf | 18 +++++ t/db_dependent/Api/V1/Messages.pm | 82 ++++++++++++++++++++++ 10 files changed, 370 insertions(+) create mode 100644 Koha/Exception/SMSDeliveryFailure.pm create mode 100644 Koha/REST/V1/Messages/Reports.pm create mode 100644 SMS/Send/Labyrintti/Driver.pm diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm index e00110a..f5c7389 100644 --- a/C4/Installer/PerlDependencies.pm +++ b/C4/Installer/PerlDependencies.pm @@ -204,6 +204,11 @@ our $PERL_DEPS = { 'required' => '1', 'min_ver' => '2.033' }, + 'LWP::Curl' => { + 'usage' => 'Curl', + 'required' => '0', + 'min_ver' => '0.12', + }, 'MIME::Base64' => { 'usage' => 'Core', 'required' => '1', diff --git a/C4/Letters.pm b/C4/Letters.pm index 0a5ec5a..c9d8494 100644 --- a/C4/Letters.pm +++ b/C4/Letters.pm @@ -1456,6 +1456,12 @@ sub _send_message_by_sms { status => 'pending', delivery_note => 'Connection failed. Attempting to resend.' } ); } + elsif ($_->isa('Koha::Exception::SMSDeliveryFailure')){ + # SMS delivery was unsuccessful. Set message to failed and give a delivery note + _set_message_status ( { message_id => $message->{'message_id'}, + status => 'failed', + delivery_note => $_->error } ); + } else { $_->rethrow(); } diff --git a/C4/SMS.pm b/C4/SMS.pm index a4ee9c7..c5573ea 100644 --- a/C4/SMS.pm +++ b/C4/SMS.pm @@ -92,6 +92,7 @@ sub send_sms { # Send a message $sent = $sender->send_sms( to => $params->{'destination'}, text => $params->{'message'}, + _message_id => $params->{'message_id'}, ); return $sent; } catch { diff --git a/Koha/Exception/SMSDeliveryFailure.pm b/Koha/Exception/SMSDeliveryFailure.pm new file mode 100644 index 0000000..0c623a9 --- /dev/null +++ b/Koha/Exception/SMSDeliveryFailure.pm @@ -0,0 +1,28 @@ +package Koha::Exception::SMSDeliveryFailure; + +# Copyright 2015 Vaara-kirjastot +# +# This file is part of Koha. +# +# Koha 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 2 of the License, or (at your option) any later +# version. +# +# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Exception::Class ( + 'Koha::Exception::SMSDeliveryFailure' => { + description => 'SMS delivery was failed.', + }, +); + +return 1; \ No newline at end of file diff --git a/Koha/REST/V1/Messages/Reports.pm b/Koha/REST/V1/Messages/Reports.pm new file mode 100644 index 0000000..5927161 --- /dev/null +++ b/Koha/REST/V1/Messages/Reports.pm @@ -0,0 +1,37 @@ +package Koha::REST::V1::Messages::Reports; + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use C4::Letters; +use Data::Dumper; + + + +=head2 list_messages($c, $args, $cb) + +Lists all messages from message_queue. + +=cut +sub create_labyrintti_report { + my ($c, $args, $cb) = @_; + + my $message = C4::Letters::GetMessage($args->{'messagenumber'}); + + return $c->$cb({ error => "No messages found." }, 404) if not $message; + + # delivery was failed. edit status to failed and add delivery note + if ($args->{'status'} eq "ERROR") { + C4::Letters::UpdateQueuedMessage({ + message_id => $message->{message_id}, + status => 'failed', + delivery_note => $args->{message}, + }); + return $c->$cb('', 201); + } + + return $c->$cb('', 204); +} + +1; diff --git a/SMS/Send/Labyrintti/Driver.pm b/SMS/Send/Labyrintti/Driver.pm new file mode 100644 index 0000000..45256b5 --- /dev/null +++ b/SMS/Send/Labyrintti/Driver.pm @@ -0,0 +1,129 @@ +=head IN THIS FILE + +This module extends the SMS::Send::Driver interface +to implement a driver compatible with the Labyrintti SMS Gateway HTTP interface. + +Module parameters are sanitated against injection attacks. + +Labyrintti responds: + + success + +phone-number OK message-count description ++358401234567 OK 1 message accepted for sending + + failure + +phone-number ERROR error-code message-count description +e.g: 12345 ERROR 2 1 message failed: Too short phone number + +=cut + +package SMS::Send::Labyrintti::Driver; +#use Modern::Perl; #Can't use this since SMS::Send uses hash keys starting with _ +use utf8; +use SMS::Send::Driver (); +use LWP::Curl; +use LWP::UserAgent; +use URI::Escape; +use C4::Context; +use Encode; +use Koha::Exception::ConnectionFailed; +use Koha::Exception::SMSDeliveryFailure; + +use vars qw{$VERSION @ISA}; +BEGIN { + $VERSION = '0.06'; + @ISA = 'SMS::Send::Driver'; +} + + +##################################################################### +# Constructor + +sub new { + my $class = shift; + my $params = {@_}; + my $username = $params->{_login} ? $params->{_login} : C4::Context->config('smsProviders')->{'labyrintti'}->{'user'}; + my $password = $params->{_password} ? $params->{_password} : C4::Context->config('smsProviders')->{'labyrintti'}->{'passwd'}; + + if (! defined $username ) { + warn "->send_sms(_login) must be defined!"; + return; + } + if (! defined $password ) { + warn "->send_sms(_password) must be defined!"; + return; + } + + #Prevent injection attack + $self->{_login} =~ s/'//g; + $self->{_password} =~ s/'//g; + + # Create the object + my $self = bless {}, $class; + + $self->{UserAgent} = LWP::UserAgent->new(timeout => 5); + $self->{_login} = $username; + $self->{_password} = $password; + + return $self; +} + +sub send_sms { + my $self = shift; + my $params = {@_}; + my $message = $params->{text}; + my $recipientNumber = $params->{to}; + + if (! defined $message ) { + warn "->send_sms(text) must be defined!"; + return; + } + if (! defined $recipientNumber ) { + warn "->send_sms(to) must be defined!"; + return; + } + + #Prevent injection attack! + $recipientNumber =~ s/'//g; + $message =~ s/(")|(\$\()|(`)/\\"/g; #Sanitate " so it won't break the system( iconv'ed curl command ) + + + + my $base_url = "https://gw.labyrintti.com:28443/sendsms"; + my $parameters = { + 'user' => $self->{_login}, + 'password' => $self->{_password}, + 'dests' => $recipientNumber, + 'text' => $message, + 'unicode' => 'yes', + }; + + my $report_url = C4::Context->config('smsProviders')->{'labyrintti'}->{'reportUrl'}; + if ($report_url) { + my $msg_id = $params->{_message_id}; + $report_url =~ s/\{messagenumber\}/$msg_id/g; + $parameters->{'report'} = $report_url; + } + + my $lwpcurl = LWP::Curl->new(auto_encode => 0); + my $return = $lwpcurl->post($base_url, $parameters); + + if ($lwpcurl->{retcode} == 6) { + Koha::Exception::ConnectionFailed->throw(error => "Connection failed"); + } + + my $delivery_note = $return; + + return 1 if ($return =~ m/OK [1-9](\d*)?/); + + # remove everything except the delivery note + $delivery_note =~ s/^(.*)message\sfailed:\s*//g; + + # pass on the error by throwing an exception - it will be eventually caught + # in C4::Letters::_send_message_by_sms() + Koha::Exception::SMSDeliveryFailure->throw(error => $delivery_note); +} + +1; diff --git a/api/v1/swagger.json b/api/v1/swagger.json index 5d37123..2e78bf3 100644 --- a/api/v1/swagger.json +++ b/api/v1/swagger.json @@ -507,6 +507,56 @@ } } } + }, + "/messages/{messagenumber}/report/labyrintti": { + "post": { + "x-mojo-controller": "Koha::REST::V1::Messages::Reports", + "operationId": "createLabyrinttiReport", + "tags": ["messages"], + "parameters": [ + { + "$ref": "#/parameters/messagenumberPathParam" + }, + { + "name": "status", + "in": "formData", + "type": "string", + "description": "Status of the delivery", + "required": true + }, + { + "name": "message", + "in": "formData", + "type": "string", + "description": "Delivery notes", + "required": true + } + ], + "consumes": ["application/x-www-form-urlencoded"], + "produces": [ + "application/json" + ], + "responses": { + "201": { + "description": "Response for creating a new report. Required status ERROR", + "schema": { + "type": "string" + } + }, + "204": { + "description": "Response for receiving delivery report with status OK", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Message not found", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } } }, "definitions": { diff --git a/etc/koha-conf.xml b/etc/koha-conf.xml index 962e031..e56c6f0 100644 --- a/etc/koha-conf.xml +++ b/etc/koha-conf.xml @@ -145,5 +145,19 @@ __PAZPAR2_TOGGLE_XML_POST__ + + + + + + + + + diff --git a/etc/koha-httpd.conf b/etc/koha-httpd.conf index 0b8078f..6c74c56 100644 --- a/etc/koha-httpd.conf +++ b/etc/koha-httpd.conf @@ -39,6 +39,15 @@ ProxyPassReverse /api/v1/ http://localhost:8081/api/v1/ ##REVERSE PROXYING OK +# Uncomment the following block to restrict access for changing +# message status via reports to only from certain IPs. +# +# Order Deny,Allow +# Deny from all +# Allow from 101.123.213.10 +# Allow from 101.123.213.11 +# + Options -Indexes @@ -168,6 +177,15 @@ ProxyPassReverse /api/v1/ http://localhost:8081/api/v1/ ##REVERSE PROXYING OK +# Uncomment the following block to restrict access for changing +# message status via reports to only from certain IPs. +# +# Order Deny,Allow +# Deny from all +# Allow from 101.123.213.10 +# Allow from 101.123.213.11 +# + Options -Indexes diff --git a/t/db_dependent/Api/V1/Messages.pm b/t/db_dependent/Api/V1/Messages.pm index 359335c..5d262df 100644 --- a/t/db_dependent/Api/V1/Messages.pm +++ b/t/db_dependent/Api/V1/Messages.pm @@ -269,4 +269,86 @@ sub delete_n_404 { return 1; } +sub post_n_reportlabyrintti404 { + my ($class, $restTest, $driver) = @_; + my $testContext = $restTest->get_testContext(); + my $activeUser = $restTest->get_activeBorrower(); + my $path = $restTest->get_routePath(); + + $path =~ s/\{messagenumber\}/12334893298007/; + $driver->post_ok( + $path => { Accept => 'application/x-www-form-urlencoded'} => + form => { + status => "OK", + message => "Message delivery succesful", + }); + $driver->status_is(404); + + return 1; +} + +sub post_n_reportlabyrintti204 { + my ($class, $restTest, $driver) = @_; + my $testContext = $restTest->get_testContext(); + my $activeUser = $restTest->get_activeBorrower(); + my $path = $restTest->get_routePath(); + + #Create the test context. + my $messages = t::lib::TestObjects::MessageQueueFactory->createTestGroup( + { + subject => "The quick brown fox", + content => "Jumps over the lazy dog.", + cardnumber => $activeUser->{'_result'}->{'_column_data'}->{cardnumber}, + message_transport_type => 'sms', + from_address => '11A001@example.com', + }, undef, $testContext, undef, undef); + + my $messagenumber = $messages->{'11A001@example.com'}->{message_id}; + $path =~ s/\{messagenumber\}/$messagenumber/; + $driver->post_ok( + $path => { Accept => 'application/x-www-form-urlencoded'} => + form => { + status => "OK", + message => "Message delivery succesful", + } + ); + $driver->status_is(204); + + t::lib::TestObjects::ObjectFactory->tearDownTestContext($testContext); + return 1; +} + +sub post_n_reportlabyrintti201 { + my ($class, $restTest, $driver) = @_; + my $testContext = $restTest->get_testContext(); + my $activeUser = $restTest->get_activeBorrower(); + my $path = $restTest->get_routePath(); + + #Create the test context. + my $messages = t::lib::TestObjects::MessageQueueFactory->createTestGroup( + { + subject => "The quick brown fox", + content => "Jumps over the lazy dog.", + cardnumber => $activeUser->{'_result'}->{'_column_data'}->{cardnumber}, + message_transport_type => 'sms', + from_address => '11A001@example.com', + }, undef, $testContext, undef, undef); + + my $messagenumber = $messages->{'11A001@example.com'}->{message_id}; + $path =~ s/\{messagenumber\}/$messagenumber/; + $driver->post_ok( + $path => { Accept => 'application/x-www-form-urlencoded'} => + form => { + status => "ERROR", + message => "Test error delivery note", + } + ); + $driver->status_is(201); + my $message = C4::Letters::GetMessage($messagenumber); + is($message->{delivery_note}, "Test error delivery note", "Delivery note for failed SMS received successfully."); + + t::lib::TestObjects::ObjectFactory->tearDownTestContext($testContext); + return 1; +} + 1; \ No newline at end of file -- 1.9.1