Bug 17252 - Koha::AuthorisedValues - Remove GetAuthorisedValueByCode
[koha.git] / C4 / Letters.pm
1 package C4::Letters;
2
3 # Copyright 2000-2002 Katipo Communications
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use strict;
21 use warnings;
22
23 use MIME::Lite;
24 use Mail::Sendmail;
25 use Date::Calc qw( Add_Delta_Days );
26 use Encode;
27 use Carp;
28 use Template;
29 use Module::Load::Conditional qw(can_load);
30
31 use C4::Members;
32 use C4::Members::Attributes qw(GetBorrowerAttributes);
33 use C4::Log;
34 use C4::SMS;
35 use C4::Debug;
36 use Koha::DateUtils;
37 use Koha::SMS::Providers;
38
39 use Koha::Email;
40 use Koha::DateUtils qw( format_sqldatetime dt_from_string );
41
42 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
43
44 BEGIN {
45     require Exporter;
46     @ISA = qw(Exporter);
47     @EXPORT = qw(
48         &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &addalert &getalert &delalert &findrelatedto &SendAlerts &GetPrintMessages &GetMessageTransportTypes
49     );
50 }
51
52 =head1 NAME
53
54 C4::Letters - Give functions for Letters management
55
56 =head1 SYNOPSIS
57
58   use C4::Letters;
59
60 =head1 DESCRIPTION
61
62   "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
63   late issues, as well as other tasks like sending a mail to users that have subscribed to a "serial issue alert" (= being warned every time a new issue has arrived at the library)
64
65   Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
66
67 =head2 GetLetters([$module])
68
69   $letters = &GetLetters($module);
70   returns informations about letters.
71   if needed, $module filters for letters given module
72
73 =cut
74
75 sub GetLetters {
76     my ($filters) = @_;
77     my $module    = $filters->{module};
78     my $code      = $filters->{code};
79     my $branchcode = $filters->{branchcode};
80     my $dbh       = C4::Context->dbh;
81     my $letters   = $dbh->selectall_arrayref(
82         q|
83             SELECT module, code, branchcode, name
84             FROM letter
85             WHERE 1
86         |
87           . ( $module ? q| AND module = ?| : q|| )
88           . ( $code   ? q| AND code = ?|   : q|| )
89           . ( defined $branchcode   ? q| AND branchcode = ?|   : q|| )
90           . q| GROUP BY code ORDER BY name|, { Slice => {} }
91         , ( $module ? $module : () )
92         , ( $code ? $code : () )
93         , ( defined $branchcode ? $branchcode : () )
94     );
95
96     return $letters;
97 }
98
99 =head2 GetLetterTemplates
100
101     my $letter_templates = GetLetterTemplates(
102         {
103             module => 'circulation',
104             code => 'my code',
105             branchcode => 'CPL', # '' for default,
106         }
107     );
108
109     Return a hashref of letter templates.
110     The key will be the message transport type.
111
112 =cut
113
114 sub GetLetterTemplates {
115     my ( $params ) = @_;
116
117     my $module    = $params->{module};
118     my $code      = $params->{code};
119     my $branchcode = $params->{branchcode} // '';
120     my $dbh       = C4::Context->dbh;
121     my $letters   = $dbh->selectall_hashref(
122         q|
123             SELECT module, code, branchcode, name, is_html, title, content, message_transport_type
124             FROM letter
125             WHERE module = ?
126             AND code = ?
127             and branchcode = ?
128         |
129         , 'message_transport_type'
130         , undef
131         , $module, $code, $branchcode
132     );
133
134     return $letters;
135 }
136
137 =head2 GetLettersAvailableForALibrary
138
139     my $letters = GetLettersAvailableForALibrary(
140         {
141             branchcode => 'CPL', # '' for default
142             module => 'circulation',
143         }
144     );
145
146     Return an arrayref of letters, sorted by name.
147     If a specific letter exist for the given branchcode, it will be retrieve.
148     Otherwise the default letter will be.
149
150 =cut
151
152 sub GetLettersAvailableForALibrary {
153     my ($filters)  = @_;
154     my $branchcode = $filters->{branchcode};
155     my $module     = $filters->{module};
156
157     croak "module should be provided" unless $module;
158
159     my $dbh             = C4::Context->dbh;
160     my $default_letters = $dbh->selectall_arrayref(
161         q|
162             SELECT module, code, branchcode, name
163             FROM letter
164             WHERE 1
165         |
166           . q| AND branchcode = ''|
167           . ( $module ? q| AND module = ?| : q|| )
168           . q| ORDER BY name|, { Slice => {} }
169         , ( $module ? $module : () )
170     );
171
172     my $specific_letters;
173     if ($branchcode) {
174         $specific_letters = $dbh->selectall_arrayref(
175             q|
176                 SELECT module, code, branchcode, name
177                 FROM letter
178                 WHERE 1
179             |
180               . q| AND branchcode = ?|
181               . ( $module ? q| AND module = ?| : q|| )
182               . q| ORDER BY name|, { Slice => {} }
183             , $branchcode
184             , ( $module ? $module : () )
185         );
186     }
187
188     my %letters;
189     for my $l (@$default_letters) {
190         $letters{ $l->{code} } = $l;
191     }
192     for my $l (@$specific_letters) {
193         # Overwrite the default letter with the specific one.
194         $letters{ $l->{code} } = $l;
195     }
196
197     return [ map { $letters{$_} }
198           sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
199           keys %letters ];
200
201 }
202
203 sub getletter {
204     my ( $module, $code, $branchcode, $message_transport_type ) = @_;
205     $message_transport_type //= '%';
206
207     if ( C4::Context->preference('IndependentBranches')
208             and $branchcode
209             and C4::Context->userenv ) {
210
211         $branchcode = C4::Context->userenv->{'branch'};
212     }
213     $branchcode //= '';
214
215     my $dbh = C4::Context->dbh;
216     my $sth = $dbh->prepare(q{
217         SELECT *
218         FROM letter
219         WHERE module=? AND code=? AND (branchcode = ? OR branchcode = '')
220         AND message_transport_type LIKE ?
221         ORDER BY branchcode DESC LIMIT 1
222     });
223     $sth->execute( $module, $code, $branchcode, $message_transport_type );
224     my $line = $sth->fetchrow_hashref
225       or return;
226     $line->{'content-type'} = 'text/html; charset="UTF-8"' if $line->{is_html};
227     return { %$line };
228 }
229
230
231 =head2 DelLetter
232
233     DelLetter(
234         {
235             branchcode => 'CPL',
236             module => 'circulation',
237             code => 'my code',
238             [ mtt => 'email', ]
239         }
240     );
241
242     Delete the letter. The mtt parameter is facultative.
243     If not given, all templates mathing the other parameters will be removed.
244
245 =cut
246
247 sub DelLetter {
248     my ($params)   = @_;
249     my $branchcode = $params->{branchcode};
250     my $module     = $params->{module};
251     my $code       = $params->{code};
252     my $mtt        = $params->{mtt};
253     my $dbh        = C4::Context->dbh;
254     $dbh->do(q|
255         DELETE FROM letter
256         WHERE branchcode = ?
257           AND module = ?
258           AND code = ?
259     | . ( $mtt ? q| AND message_transport_type = ?| : q|| )
260     , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ) );
261 }
262
263 =head2 addalert ($borrowernumber, $type, $externalid)
264
265     parameters : 
266     - $borrowernumber : the number of the borrower subscribing to the alert
267     - $type : the type of alert.
268     - $externalid : the primary key of the object to put alert on. For issues, the alert is made on subscriptionid.
269     
270     create an alert and return the alertid (primary key)
271
272 =cut
273
274 sub addalert {
275     my ( $borrowernumber, $type, $externalid ) = @_;
276     my $dbh = C4::Context->dbh;
277     my $sth =
278       $dbh->prepare(
279         "insert into alert (borrowernumber, type, externalid) values (?,?,?)");
280     $sth->execute( $borrowernumber, $type, $externalid );
281
282     # get the alert number newly created and return it
283     my $alertid = $dbh->{'mysql_insertid'};
284     return $alertid;
285 }
286
287 =head2 delalert ($alertid)
288
289     parameters :
290     - alertid : the alert id
291     deletes the alert
292
293 =cut
294
295 sub delalert {
296     my $alertid = shift or die "delalert() called without valid argument (alertid)";    # it's gonna die anyway.
297     $debug and warn "delalert: deleting alertid $alertid";
298     my $sth = C4::Context->dbh->prepare("delete from alert where alertid=?");
299     $sth->execute($alertid);
300 }
301
302 =head2 getalert ([$borrowernumber], [$type], [$externalid])
303
304     parameters :
305     - $borrowernumber : the number of the borrower subscribing to the alert
306     - $type : the type of alert.
307     - $externalid : the primary key of the object to put alert on. For issues, the alert is made on subscriptionid.
308     all parameters NON mandatory. If a parameter is omitted, the query is done without the corresponding parameter. For example, without $externalid, returns all alerts for a borrower on a topic.
309
310 =cut
311
312 sub getalert {
313     my ( $borrowernumber, $type, $externalid ) = @_;
314     my $dbh   = C4::Context->dbh;
315     my $query = "SELECT a.*, b.branchcode FROM alert a JOIN borrowers b USING(borrowernumber) WHERE 1";
316     my @bind;
317     if ($borrowernumber and $borrowernumber =~ /^\d+$/) {
318         $query .= " AND borrowernumber=?";
319         push @bind, $borrowernumber;
320     }
321     if ($type) {
322         $query .= " AND type=?";
323         push @bind, $type;
324     }
325     if ($externalid) {
326         $query .= " AND externalid=?";
327         push @bind, $externalid;
328     }
329     my $sth = $dbh->prepare($query);
330     $sth->execute(@bind);
331     return $sth->fetchall_arrayref({});
332 }
333
334 =head2 findrelatedto($type, $externalid)
335
336     parameters :
337     - $type : the type of alert
338     - $externalid : the id of the "object" to query
339
340     In the table alert, a "id" is stored in the externalid field. This "id" is related to another table, depending on the type of the alert.
341     When type=issue, the id is related to a subscriptionid and this sub returns the name of the biblio.
342
343 =cut
344     
345 # outmoded POD:
346 # When type=virtual, the id is related to a virtual shelf and this sub returns the name of the sub
347
348 sub findrelatedto {
349     my $type       = shift or return;
350     my $externalid = shift or return;
351     my $q = ($type eq 'issue'   ) ?
352 "select title as result from subscription left join biblio on subscription.biblionumber=biblio.biblionumber where subscriptionid=?" :
353             ($type eq 'borrower') ?
354 "select concat(firstname,' ',surname) from borrowers where borrowernumber=?" : undef;
355     unless ($q) {
356         warn "findrelatedto(): Illegal type '$type'";
357         return;
358     }
359     my $sth = C4::Context->dbh->prepare($q);
360     $sth->execute($externalid);
361     my ($result) = $sth->fetchrow;
362     return $result;
363 }
364
365 =head2 SendAlerts
366
367     parameters :
368     - $type : the type of alert
369     - $externalid : the id of the "object" to query
370     - $letter_code : the letter to send.
371
372     send an alert to all borrowers having put an alert on a given subject.
373
374     Returns undef or { error => 'message } on failure.
375     Returns true on success.
376
377 =cut
378
379 sub SendAlerts {
380     my ( $type, $externalid, $letter_code ) = @_;
381     my $dbh = C4::Context->dbh;
382     if ( $type eq 'issue' ) {
383
384         # prepare the letter...
385         # search the subscriptionid
386         my $sth =
387           $dbh->prepare(
388             "SELECT subscriptionid FROM serial WHERE serialid=?");
389         $sth->execute($externalid);
390         my ($subscriptionid) = $sth->fetchrow
391           or warn( "No subscription for '$externalid'" ),
392              return;
393
394         # search the biblionumber
395         $sth =
396           $dbh->prepare(
397             "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
398         $sth->execute($subscriptionid);
399         my ($biblionumber) = $sth->fetchrow
400           or warn( "No biblionumber for '$subscriptionid'" ),
401              return;
402
403         my %letter;
404         # find the list of borrowers to alert
405         my $alerts = getalert( '', 'issue', $subscriptionid );
406         foreach (@$alerts) {
407             my $borinfo = C4::Members::GetMember('borrowernumber' => $_->{'borrowernumber'});
408             my $email = $borinfo->{email} or next;
409
410 #                    warn "sending issues...";
411             my $userenv = C4::Context->userenv;
412             my $library = Koha::Libraries->find( $_->{branchcode} );
413             my $letter = GetPreparedLetter (
414                 module => 'serial',
415                 letter_code => $letter_code,
416                 branchcode => $userenv->{branch},
417                 tables => {
418                     'branches'    => $_->{branchcode},
419                     'biblio'      => $biblionumber,
420                     'biblioitems' => $biblionumber,
421                     'borrowers'   => $borinfo,
422                     'subscription' => $subscriptionid,
423                     'serial' => $externalid,
424                 },
425                 want_librarian => 1,
426             ) or return;
427
428             # ... then send mail
429             my $message = Koha::Email->new();
430             my %mail = $message->create_message_headers(
431                 {
432                     to      => $email,
433                     from    => $library->branchemail,
434                     replyto => $library->branchreplyto,
435                     sender  => $library->branchreturnpath,
436                     subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
437                     message => $letter->{'is_html'}
438                                 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
439                                               Encode::encode( "UTF-8", "" . $letter->{'title'} ))
440                                 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
441                     contenttype => $letter->{'is_html'}
442                                     ? 'text/html; charset="utf-8"'
443                                     : 'text/plain; charset="utf-8"',
444                 }
445             );
446             unless( sendmail(%mail) ) {
447                 carp $Mail::Sendmail::error;
448                 return { error => $Mail::Sendmail::error };
449             }
450         }
451     }
452     elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' ) {
453
454         # prepare the letter...
455         # search the biblionumber
456         my $strsth =  $type eq 'claimacquisition'
457             ? qq{
458             SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
459             FROM aqorders
460             LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
461             LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
462             LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
463             WHERE aqorders.ordernumber IN (
464             }
465             : qq{
466             SELECT serial.*,subscription.*, biblio.*, aqbooksellers.*,
467             aqbooksellers.id AS booksellerid
468             FROM serial
469             LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
470             LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
471             LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
472             WHERE serial.serialid IN (
473             };
474
475         if (!@$externalid){
476             carp "No Order selected";
477             return { error => "no_order_selected" };
478         }
479
480         $strsth .= join( ",", @$externalid ) . ")";
481         my $sthorders = $dbh->prepare($strsth);
482         $sthorders->execute;
483         my $dataorders = $sthorders->fetchall_arrayref( {} );
484
485         my $sthbookseller =
486           $dbh->prepare("select * from aqbooksellers where id=?");
487         $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
488         my $databookseller = $sthbookseller->fetchrow_hashref;
489         my $addressee =  $type eq 'claimacquisition' ? 'acqprimary' : 'serialsprimary';
490         my $sthcontact =
491           $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
492         $sthcontact->execute( $dataorders->[0]->{booksellerid} );
493         my $datacontact = $sthcontact->fetchrow_hashref;
494
495         my @email;
496         my @cc;
497         push @email, $databookseller->{bookselleremail} if $databookseller->{bookselleremail};
498         push @email, $datacontact->{email}           if ( $datacontact && $datacontact->{email} );
499         unless (@email) {
500             warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
501             return { error => "no_email" };
502         }
503         my $addlcontact;
504         while ($addlcontact = $sthcontact->fetchrow_hashref) {
505             push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
506         }
507
508         my $userenv = C4::Context->userenv;
509         my $letter = GetPreparedLetter (
510             module => $type,
511             letter_code => $letter_code,
512             branchcode => $userenv->{branch},
513             tables => {
514                 'branches'    => $userenv->{branch},
515                 'aqbooksellers' => $databookseller,
516                 'aqcontacts'    => $datacontact,
517             },
518             repeat => $dataorders,
519             want_librarian => 1,
520         ) or return;
521
522         # Remove the order tag
523         $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
524
525         # ... then send mail
526         my %mail = (
527             To => join( ',', @email),
528             Cc             => join( ',', @cc),
529             From           => $userenv->{emailaddress},
530             Subject        => Encode::encode( "UTF-8", "" . $letter->{title} ),
531             Message => $letter->{'is_html'}
532                             ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
533                                           Encode::encode( "UTF-8", "" . $letter->{'title'} ))
534                             : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
535             'Content-Type' => $letter->{'is_html'}
536                                 ? 'text/html; charset="utf-8"'
537                                 : 'text/plain; charset="utf-8"',
538         );
539
540         $mail{'Reply-to'} = C4::Context->preference('ReplytoDefault')
541           if C4::Context->preference('ReplytoDefault');
542         $mail{'Sender'} = C4::Context->preference('ReturnpathDefault')
543           if C4::Context->preference('ReturnpathDefault');
544         $mail{'Bcc'} = $userenv->{emailaddress}
545           if C4::Context->preference("ClaimsBccCopy");
546
547         unless ( sendmail(%mail) ) {
548             carp $Mail::Sendmail::error;
549             return { error => $Mail::Sendmail::error };
550         }
551
552         logaction(
553             "ACQUISITION",
554             $type eq 'claimissues' ? "CLAIM ISSUE" : "ACQUISITION CLAIM",
555             undef,
556             "To="
557                 . join( ',', @email )
558                 . " Title="
559                 . $letter->{title}
560                 . " Content="
561                 . $letter->{content}
562         ) if C4::Context->preference("LetterLog");
563     }
564    # send an "account details" notice to a newly created user
565     elsif ( $type eq 'members' ) {
566         my $library = Koha::Libraries->find( $externalid->{branchcode} )->unblessed;
567         my $letter = GetPreparedLetter (
568             module => 'members',
569             letter_code => $letter_code,
570             branchcode => $externalid->{'branchcode'},
571             tables => {
572                 'branches'    => $library,
573                 'borrowers' => $externalid->{'borrowernumber'},
574             },
575             substitute => { 'borrowers.password' => $externalid->{'password'} },
576             want_librarian => 1,
577         ) or return;
578         return { error => "no_email" } unless $externalid->{'emailaddr'};
579         my $email = Koha::Email->new();
580         my %mail  = $email->create_message_headers(
581             {
582                 to      => $externalid->{'emailaddr'},
583                 from    => $library->{branchemail},
584                 replyto => $library->{branchreplyto},
585                 sender  => $library->{branchreturnpath},
586                 subject => Encode::encode( "UTF-8", "" . $letter->{'title'} ),
587                 message => $letter->{'is_html'}
588                             ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
589                                           Encode::encode( "UTF-8", "" . $letter->{'title'}  ) )
590                             : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
591                 contenttype => $letter->{'is_html'}
592                                 ? 'text/html; charset="utf-8"'
593                                 : 'text/plain; charset="utf-8"',
594             }
595         );
596         unless( sendmail(%mail) ) {
597             carp $Mail::Sendmail::error;
598             return { error => $Mail::Sendmail::error };
599         }
600     }
601
602     # If we come here, return an OK status
603     return 1;
604 }
605
606 =head2 GetPreparedLetter( %params )
607
608     %params hash:
609       module => letter module, mandatory
610       letter_code => letter code, mandatory
611       branchcode => for letter selection, if missing default system letter taken
612       tables => a hashref with table names as keys. Values are either:
613         - a scalar - primary key value
614         - an arrayref - primary key values
615         - a hashref - full record
616       substitute => custom substitution key/value pairs
617       repeat => records to be substituted on consecutive lines:
618         - an arrayref - tries to guess what needs substituting by
619           taking remaining << >> tokensr; not recommended
620         - a hashref token => @tables - replaces <token> << >> << >> </token>
621           subtemplate for each @tables row; table is a hashref as above
622       want_librarian => boolean,  if set to true triggers librarian details
623         substitution from the userenv
624     Return value:
625       letter fields hashref (title & content useful)
626
627 =cut
628
629 sub GetPreparedLetter {
630     my %params = @_;
631
632     my $module      = $params{module} or croak "No module";
633     my $letter_code = $params{letter_code} or croak "No letter_code";
634     my $branchcode  = $params{branchcode} || '';
635     my $mtt         = $params{message_transport_type} || 'email';
636
637     my $letter = getletter( $module, $letter_code, $branchcode, $mtt )
638         or warn( "No $module $letter_code letter transported by " . $mtt ),
639             return;
640
641     my $tables = $params{tables};
642     my $substitute = $params{substitute};
643     my $repeat = $params{repeat};
644     $tables || $substitute || $repeat
645       or carp( "ERROR: nothing to substitute - both 'tables' and 'substitute' are empty" ),
646          return;
647     my $want_librarian = $params{want_librarian};
648
649     if ($substitute) {
650         while ( my ($token, $val) = each %$substitute ) {
651             if ( $token eq 'items.content' ) {
652                 $val =~ s|\n|<br/>|g if $letter->{is_html};
653             }
654
655             $letter->{title} =~ s/<<$token>>/$val/g;
656             $letter->{content} =~ s/<<$token>>/$val/g;
657        }
658     }
659
660     my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
661     $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
662
663     if ($want_librarian) {
664         # parsing librarian name
665         my $userenv = C4::Context->userenv;
666         $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
667         $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
668         $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
669     }
670
671     my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
672
673     if ($repeat) {
674         if (ref ($repeat) eq 'ARRAY' ) {
675             $repeat_no_enclosing_tags = $repeat;
676         } else {
677             $repeat_enclosing_tags = $repeat;
678         }
679     }
680
681     if ($repeat_enclosing_tags) {
682         while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
683             if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
684                 my $subcontent = $1;
685                 my @lines = map {
686                     my %subletter = ( title => '', content => $subcontent );
687                     _substitute_tables( \%subletter, $_ );
688                     $subletter{content};
689                 } @$tag_tables;
690                 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
691             }
692         }
693     }
694
695     if ($tables) {
696         _substitute_tables( $letter, $tables );
697     }
698
699     if ($repeat_no_enclosing_tags) {
700         if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
701             my $line = $&;
702             my $i = 1;
703             my @lines = map {
704                 my $c = $line;
705                 $c =~ s/<<count>>/$i/go;
706                 foreach my $field ( keys %{$_} ) {
707                     $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
708                 }
709                 $i++;
710                 $c;
711             } @$repeat_no_enclosing_tags;
712
713             my $replaceby = join( "\n", @lines );
714             $letter->{content} =~ s/\Q$line\E/$replaceby/s;
715         }
716     }
717
718     $letter->{content} = _process_tt(
719         {
720             content => $letter->{content},
721             tables  => $tables,
722         }
723     );
724
725     $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
726
727     return $letter;
728 }
729
730 sub _substitute_tables {
731     my ( $letter, $tables ) = @_;
732     while ( my ($table, $param) = each %$tables ) {
733         next unless $param;
734
735         my $ref = ref $param;
736
737         my $values;
738         if ($ref && $ref eq 'HASH') {
739             $values = $param;
740         }
741         else {
742             my $sth = _parseletter_sth($table);
743             unless ($sth) {
744                 warn "_parseletter_sth('$table') failed to return a valid sth.  No substitution will be done for that table.";
745                 return;
746             }
747             $sth->execute( $ref ? @$param : $param );
748
749             $values = $sth->fetchrow_hashref;
750             $sth->finish();
751         }
752
753         _parseletter ( $letter, $table, $values );
754     }
755 }
756
757 sub _parseletter_sth {
758     my $table = shift;
759     my $sth;
760     unless ($table) {
761         carp "ERROR: _parseletter_sth() called without argument (table)";
762         return;
763     }
764     # NOTE: we used to check whether we had a statement handle cached in
765     #       a %handles module-level variable. This was a dumb move and
766     #       broke things for the rest of us. prepare_cached is a better
767     #       way to cache statement handles anyway.
768     my $query = 
769     ($table eq 'biblio'       ) ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
770     ($table eq 'biblioitems'  ) ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
771     ($table eq 'items'        ) ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
772     ($table eq 'issues'       ) ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
773     ($table eq 'old_issues'   ) ? "SELECT * FROM $table WHERE     itemnumber = ? ORDER BY timestamp DESC LIMIT 1"  :
774     ($table eq 'reserves'     ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?"             :
775     ($table eq 'borrowers'    ) ? "SELECT * FROM $table WHERE borrowernumber = ?"                                  :
776     ($table eq 'branches'     ) ? "SELECT * FROM $table WHERE     branchcode = ?"                                  :
777     ($table eq 'suggestions'  ) ? "SELECT * FROM $table WHERE   suggestionid = ?"                                  :
778     ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE             id = ?"                                  :
779     ($table eq 'aqorders'     ) ? "SELECT * FROM $table WHERE    ordernumber = ?"                                  :
780     ($table eq 'opac_news'    ) ? "SELECT * FROM $table WHERE          idnew = ?"                                  :
781     ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
782     ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
783     ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
784     undef ;
785     unless ($query) {
786         warn "ERROR: No _parseletter_sth query for table '$table'";
787         return;     # nothing to get
788     }
789     unless ($sth = C4::Context->dbh->prepare_cached($query)) {
790         warn "ERROR: Failed to prepare query: '$query'";
791         return;
792     }
793     return $sth;    # now cache is populated for that $table
794 }
795
796 =head2 _parseletter($letter, $table, $values)
797
798     parameters :
799     - $letter : a hash to letter fields (title & content useful)
800     - $table : the Koha table to parse.
801     - $values_in : table record hashref
802     parse all fields from a table, and replace values in title & content with the appropriate value
803     (not exported sub, used only internally)
804
805 =cut
806
807 sub _parseletter {
808     my ( $letter, $table, $values_in ) = @_;
809
810     # Work on a local copy of $values_in (passed by reference) to avoid side effects
811     # in callers ( by changing / formatting values )
812     my $values = $values_in ? { %$values_in } : {};
813
814     if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
815         $values->{'dateexpiry'} = format_sqldatetime( $values->{'dateexpiry'} );
816     }
817
818     if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
819         my @waitingdate = split /-/, $values->{'waitingdate'};
820
821         $values->{'expirationdate'} = '';
822         if ( C4::Context->preference('ReservesMaxPickUpDelay') ) {
823             my $dt = dt_from_string();
824             $dt->add( days => C4::Context->preference('ReservesMaxPickUpDelay') );
825             $values->{'expirationdate'} = output_pref( { dt => $dt, dateonly => 1 } );
826         }
827
828         $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
829
830     }
831
832     if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
833         my $todaysdate = output_pref( DateTime->now() );
834         $letter->{content} =~ s/<<today>>/$todaysdate/go;
835     }
836
837     while ( my ($field, $val) = each %$values ) {
838         $val =~ s/\p{P}$// if $val && $table=~/biblio/;
839             #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
840             #Therefore adding the test on biblio. This includes biblioitems,
841             #but excludes items. Removed unneeded global and lookahead.
842
843         if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
844             my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
845             $val = $av->count ? $av->next->lib : '';
846         }
847
848         # Dates replacement
849         my $replacedby   = defined ($val) ? $val : '';
850         if (    $replacedby
851             and not $replacedby =~ m|0000-00-00|
852             and not $replacedby =~ m|9999-12-31|
853             and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
854         {
855             # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
856             my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
857             my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
858
859             for my $letter_field ( qw( title content ) ) {
860                 my $filter_string_used = q{};
861                 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
862                     # We overwrite $dateonly if the filter exists and we have a time in the datetime
863                     $filter_string_used = $1 || q{};
864                     $dateonly = $1 unless $dateonly;
865                 }
866                 my $replacedby_date = eval {
867                     output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
868                 };
869
870                 if ( $letter->{ $letter_field } ) {
871                     $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
872                     $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
873                 }
874             }
875         }
876         # Other fields replacement
877         else {
878             for my $letter_field ( qw( title content ) ) {
879                 if ( $letter->{ $letter_field } ) {
880                     $letter->{ $letter_field }   =~ s/<<$table.$field>>/$replacedby/g;
881                     $letter->{ $letter_field }   =~ s/<<$field>>/$replacedby/g;
882                 }
883             }
884         }
885     }
886
887     if ($table eq 'borrowers' && $letter->{content}) {
888         if ( my $attributes = GetBorrowerAttributes($values->{borrowernumber}) ) {
889             my %attr;
890             foreach (@$attributes) {
891                 my $code = $_->{code};
892                 my $val  = $_->{value_description} || $_->{value};
893                 $val =~ s/\p{P}(?=$)//g if $val;
894                 next unless $val gt '';
895                 $attr{$code} ||= [];
896                 push @{ $attr{$code} }, $val;
897             }
898             while ( my ($code, $val_ar) = each %attr ) {
899                 my $replacefield = "<<borrower-attribute:$code>>";
900                 my $replacedby   = join ',', @$val_ar;
901                 $letter->{content} =~ s/$replacefield/$replacedby/g;
902             }
903         }
904     }
905     return $letter;
906 }
907
908 =head2 EnqueueLetter
909
910   my $success = EnqueueLetter( { letter => $letter, 
911         borrowernumber => '12', message_transport_type => 'email' } )
912
913 places a letter in the message_queue database table, which will
914 eventually get processed (sent) by the process_message_queue.pl
915 cronjob when it calls SendQueuedMessages.
916
917 return message_id on success
918
919 =cut
920
921 sub EnqueueLetter {
922     my $params = shift or return;
923
924     return unless exists $params->{'letter'};
925 #   return unless exists $params->{'borrowernumber'};
926     return unless exists $params->{'message_transport_type'};
927
928     my $content = $params->{letter}->{content};
929     $content =~ s/\s+//g if(defined $content);
930     if ( not defined $content or $content eq '' ) {
931         warn "Trying to add an empty message to the message queue" if $debug;
932         return;
933     }
934
935     # If we have any attachments we should encode then into the body.
936     if ( $params->{'attachments'} ) {
937         $params->{'letter'} = _add_attachments(
938             {   letter      => $params->{'letter'},
939                 attachments => $params->{'attachments'},
940                 message     => MIME::Lite->new( Type => 'multipart/mixed' ),
941             }
942         );
943     }
944
945     my $dbh       = C4::Context->dbh();
946     my $statement = << 'ENDSQL';
947 INSERT INTO message_queue
948 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type )
949 VALUES
950 ( ?,              ?,       ?,       ?,        ?,           ?,                      ?,      NOW(),       ?,          ?,            ? )
951 ENDSQL
952
953     my $sth    = $dbh->prepare($statement);
954     my $result = $sth->execute(
955         $params->{'borrowernumber'},              # borrowernumber
956         $params->{'letter'}->{'title'},           # subject
957         $params->{'letter'}->{'content'},         # content
958         $params->{'letter'}->{'metadata'} || '',  # metadata
959         $params->{'letter'}->{'code'}     || '',  # letter_code
960         $params->{'message_transport_type'},      # message_transport_type
961         'pending',                                # status
962         $params->{'to_address'},                  # to_address
963         $params->{'from_address'},                # from_address
964         $params->{'letter'}->{'content-type'},    # content_type
965     );
966     return $dbh->last_insert_id(undef,undef,'message_queue', undef);
967 }
968
969 =head2 SendQueuedMessages ([$hashref]) 
970
971   my $sent = SendQueuedMessages( { verbose => 1 } );
972
973 sends all of the 'pending' items in the message queue.
974
975 returns number of messages sent.
976
977 =cut
978
979 sub SendQueuedMessages {
980     my $params = shift;
981
982     my $unsent_messages = _get_unsent_messages();
983     MESSAGE: foreach my $message ( @$unsent_messages ) {
984         # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
985         warn sprintf( 'sending %s message to patron: %s',
986                       $message->{'message_transport_type'},
987                       $message->{'borrowernumber'} || 'Admin' )
988           if $params->{'verbose'} or $debug;
989         # This is just begging for subclassing
990         next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
991         if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
992             _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
993         }
994         elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
995             if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
996                 my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
997                 my $sms_provider = Koha::SMS::Providers->find( $member->{'sms_provider_id'} );
998                 $message->{to_address} .= '@' . $sms_provider->domain();
999                 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1000             } else {
1001                 _send_message_by_sms( $message );
1002             }
1003         }
1004     }
1005     return scalar( @$unsent_messages );
1006 }
1007
1008 =head2 GetRSSMessages
1009
1010   my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1011
1012 returns a listref of all queued RSS messages for a particular person.
1013
1014 =cut
1015
1016 sub GetRSSMessages {
1017     my $params = shift;
1018
1019     return unless $params;
1020     return unless ref $params;
1021     return unless $params->{'borrowernumber'};
1022     
1023     return _get_unsent_messages( { message_transport_type => 'rss',
1024                                    limit                  => $params->{'limit'},
1025                                    borrowernumber         => $params->{'borrowernumber'}, } );
1026 }
1027
1028 =head2 GetPrintMessages
1029
1030   my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1031
1032 Returns a arrayref of all queued print messages (optionally, for a particular
1033 person).
1034
1035 =cut
1036
1037 sub GetPrintMessages {
1038     my $params = shift || {};
1039     
1040     return _get_unsent_messages( { message_transport_type => 'print',
1041                                    borrowernumber         => $params->{'borrowernumber'},
1042                                  } );
1043 }
1044
1045 =head2 GetQueuedMessages ([$hashref])
1046
1047   my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1048
1049 fetches messages out of the message queue.
1050
1051 returns:
1052 list of hashes, each has represents a message in the message queue.
1053
1054 =cut
1055
1056 sub GetQueuedMessages {
1057     my $params = shift;
1058
1059     my $dbh = C4::Context->dbh();
1060     my $statement = << 'ENDSQL';
1061 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued
1062 FROM message_queue
1063 ENDSQL
1064
1065     my @query_params;
1066     my @whereclauses;
1067     if ( exists $params->{'borrowernumber'} ) {
1068         push @whereclauses, ' borrowernumber = ? ';
1069         push @query_params, $params->{'borrowernumber'};
1070     }
1071
1072     if ( @whereclauses ) {
1073         $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1074     }
1075
1076     if ( defined $params->{'limit'} ) {
1077         $statement .= ' LIMIT ? ';
1078         push @query_params, $params->{'limit'};
1079     }
1080
1081     my $sth = $dbh->prepare( $statement );
1082     my $result = $sth->execute( @query_params );
1083     return $sth->fetchall_arrayref({});
1084 }
1085
1086 =head2 GetMessageTransportTypes
1087
1088   my @mtt = GetMessageTransportTypes();
1089
1090   returns an arrayref of transport types
1091
1092 =cut
1093
1094 sub GetMessageTransportTypes {
1095     my $dbh = C4::Context->dbh();
1096     my $mtts = $dbh->selectcol_arrayref("
1097         SELECT message_transport_type
1098         FROM message_transport_types
1099         ORDER BY message_transport_type
1100     ");
1101     return $mtts;
1102 }
1103
1104 =head2 GetMessage
1105
1106     my $message = C4::Letters::Message($message_id);
1107
1108 =cut
1109
1110 sub GetMessage {
1111     my ( $message_id ) = @_;
1112     return unless $message_id;
1113     my $dbh = C4::Context->dbh;
1114     return $dbh->selectrow_hashref(q|
1115         SELECT message_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type
1116         FROM message_queue
1117         WHERE message_id = ?
1118     |, {}, $message_id );
1119 }
1120
1121 =head2 ResendMessage
1122
1123   Attempt to resend a message which has failed previously.
1124
1125   my $has_been_resent = C4::Letters::ResendMessage($message_id);
1126
1127   Updates the message to 'pending' status so that
1128   it will be resent later on.
1129
1130   returns 1 on success, 0 on failure, undef if no message was found
1131
1132 =cut
1133
1134 sub ResendMessage {
1135     my $message_id = shift;
1136     return unless $message_id;
1137
1138     my $message = GetMessage( $message_id );
1139     return unless $message;
1140     my $rv = 0;
1141     if ( $message->{status} ne 'pending' ) {
1142         $rv = C4::Letters::_set_message_status({
1143             message_id => $message_id,
1144             status => 'pending',
1145         });
1146         $rv = $rv > 0? 1: 0;
1147         # Clear destination email address to force address update
1148         _update_message_to_address( $message_id, undef ) if $rv &&
1149             $message->{message_transport_type} eq 'email';
1150     }
1151     return $rv;
1152 }
1153
1154 =head2 _add_attachements
1155
1156 named parameters:
1157 letter - the standard letter hashref
1158 attachments - listref of attachments. each attachment is a hashref of:
1159   type - the mime type, like 'text/plain'
1160   content - the actual attachment
1161   filename - the name of the attachment.
1162 message - a MIME::Lite object to attach these to.
1163
1164 returns your letter object, with the content updated.
1165
1166 =cut
1167
1168 sub _add_attachments {
1169     my $params = shift;
1170
1171     my $letter = $params->{'letter'};
1172     my $attachments = $params->{'attachments'};
1173     return $letter unless @$attachments;
1174     my $message = $params->{'message'};
1175
1176     # First, we have to put the body in as the first attachment
1177     $message->attach(
1178         Type => $letter->{'content-type'} || 'TEXT',
1179         Data => $letter->{'is_html'}
1180             ? _wrap_html($letter->{'content'}, $letter->{'title'})
1181             : $letter->{'content'},
1182     );
1183
1184     foreach my $attachment ( @$attachments ) {
1185         $message->attach(
1186             Type     => $attachment->{'type'},
1187             Data     => $attachment->{'content'},
1188             Filename => $attachment->{'filename'},
1189         );
1190     }
1191     # we're forcing list context here to get the header, not the count back from grep.
1192     ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1193     $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1194     $letter->{'content'} = $message->body_as_string;
1195
1196     return $letter;
1197
1198 }
1199
1200 sub _get_unsent_messages {
1201     my $params = shift;
1202
1203     my $dbh = C4::Context->dbh();
1204     my $statement = << 'ENDSQL';
1205 SELECT mq.message_id, mq.borrowernumber, mq.subject, mq.content, mq.message_transport_type, mq.status, mq.time_queued, mq.from_address, mq.to_address, mq.content_type, b.branchcode, mq.letter_code
1206   FROM message_queue mq
1207   LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1208  WHERE status = ?
1209 ENDSQL
1210
1211     my @query_params = ('pending');
1212     if ( ref $params ) {
1213         if ( $params->{'message_transport_type'} ) {
1214             $statement .= ' AND message_transport_type = ? ';
1215             push @query_params, $params->{'message_transport_type'};
1216         }
1217         if ( $params->{'borrowernumber'} ) {
1218             $statement .= ' AND borrowernumber = ? ';
1219             push @query_params, $params->{'borrowernumber'};
1220         }
1221         if ( $params->{'limit'} ) {
1222             $statement .= ' limit ? ';
1223             push @query_params, $params->{'limit'};
1224         }
1225     }
1226
1227     $debug and warn "_get_unsent_messages SQL: $statement";
1228     $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1229     my $sth = $dbh->prepare( $statement );
1230     my $result = $sth->execute( @query_params );
1231     return $sth->fetchall_arrayref({});
1232 }
1233
1234 sub _send_message_by_email {
1235     my $message = shift or return;
1236     my ($username, $password, $method) = @_;
1237
1238     my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
1239     my $to_address = $message->{'to_address'};
1240     unless ($to_address) {
1241         unless ($member) {
1242             warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1243             _set_message_status( { message_id => $message->{'message_id'},
1244                                    status     => 'failed' } );
1245             return;
1246         }
1247         $to_address = C4::Members::GetNoticeEmailAddress( $message->{'borrowernumber'} );
1248         unless ($to_address) {  
1249             # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1250             # warning too verbose for this more common case?
1251             _set_message_status( { message_id => $message->{'message_id'},
1252                                    status     => 'failed' } );
1253             return;
1254         }
1255     }
1256
1257     my $utf8   = decode('MIME-Header', $message->{'subject'} );
1258     $message->{subject}= encode('MIME-Header', $utf8);
1259     my $subject = encode('UTF-8', $message->{'subject'});
1260     my $content = encode('UTF-8', $message->{'content'});
1261     my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1262     my $is_html = $content_type =~ m/html/io;
1263     my $branch_email = undef;
1264     my $branch_replyto = undef;
1265     my $branch_returnpath = undef;
1266     if ($member) {
1267         my $library = Koha::Libraries->find( $member->{branchcode} );
1268         $branch_email      = $library->branchemail;
1269         $branch_replyto    = $library->branchreplyto;
1270         $branch_returnpath = $library->branchreturnpath;
1271     }
1272     my $email = Koha::Email->new();
1273     my %sendmail_params = $email->create_message_headers(
1274         {
1275             to      => $to_address,
1276             from    => $message->{'from_address'} || $branch_email,
1277             replyto => $branch_replyto,
1278             sender  => $branch_returnpath,
1279             subject => $subject,
1280             message => $is_html ? _wrap_html( $content, $subject ) : $content,
1281             contenttype => $content_type
1282         }
1283     );
1284
1285     $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
1286     if ( my $bcc = C4::Context->preference('OverdueNoticeBcc') ) {
1287        $sendmail_params{ Bcc } = $bcc;
1288     }
1289
1290     _update_message_to_address($message->{'message_id'},$to_address) unless $message->{to_address}; #if initial message address was empty, coming here means that a to address was found and queue should be updated
1291
1292     if ( sendmail( %sendmail_params ) ) {
1293         _set_message_status( { message_id => $message->{'message_id'},
1294                 status     => 'sent' } );
1295         return 1;
1296     } else {
1297         _set_message_status( { message_id => $message->{'message_id'},
1298                 status     => 'failed' } );
1299         carp $Mail::Sendmail::error;
1300         return;
1301     }
1302 }
1303
1304 sub _wrap_html {
1305     my ($content, $title) = @_;
1306
1307     my $css = C4::Context->preference("NoticeCSS") || '';
1308     $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1309     return <<EOS;
1310 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1311     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1312 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1313 <head>
1314 <title>$title</title>
1315 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1316 $css
1317 </head>
1318 <body>
1319 $content
1320 </body>
1321 </html>
1322 EOS
1323 }
1324
1325 sub _is_duplicate {
1326     my ( $message ) = @_;
1327     my $dbh = C4::Context->dbh;
1328     my $count = $dbh->selectrow_array(q|
1329         SELECT COUNT(*)
1330         FROM message_queue
1331         WHERE message_transport_type = ?
1332         AND borrowernumber = ?
1333         AND letter_code = ?
1334         AND CAST(time_queued AS date) = CAST(NOW() AS date)
1335         AND status="sent"
1336         AND content = ?
1337     |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1338     return $count;
1339 }
1340
1341 sub _send_message_by_sms {
1342     my $message = shift or return;
1343     my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
1344
1345     unless ( $member->{smsalertnumber} ) {
1346         _set_message_status( { message_id => $message->{'message_id'},
1347                                status     => 'failed' } );
1348         return;
1349     }
1350
1351     if ( _is_duplicate( $message ) ) {
1352         _set_message_status( { message_id => $message->{'message_id'},
1353                                status     => 'failed' } );
1354         return;
1355     }
1356
1357     my $success = C4::SMS->send_sms( { destination => $member->{'smsalertnumber'},
1358                                        message     => $message->{'content'},
1359                                      } );
1360     _set_message_status( { message_id => $message->{'message_id'},
1361                            status     => ($success ? 'sent' : 'failed') } );
1362     return $success;
1363 }
1364
1365 sub _update_message_to_address {
1366     my ($id, $to)= @_;
1367     my $dbh = C4::Context->dbh();
1368     $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1369 }
1370
1371 sub _set_message_status {
1372     my $params = shift or return;
1373
1374     foreach my $required_parameter ( qw( message_id status ) ) {
1375         return unless exists $params->{ $required_parameter };
1376     }
1377
1378     my $dbh = C4::Context->dbh();
1379     my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1380     my $sth = $dbh->prepare( $statement );
1381     my $result = $sth->execute( $params->{'status'},
1382                                 $params->{'message_id'} );
1383     return $result;
1384 }
1385
1386 sub _process_tt {
1387     my ( $params ) = @_;
1388
1389     my $content = $params->{content};
1390     my $tables = $params->{tables};
1391
1392     my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1393     my $template           = Template->new(
1394         {
1395             EVAL_PERL    => 1,
1396             ABSOLUTE     => 1,
1397             PLUGIN_BASE  => 'Koha::Template::Plugin',
1398             COMPILE_EXT  => $use_template_cache ? '.ttc' : '',
1399             COMPILE_DIR  => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1400             FILTERS      => {},
1401             ENCODING     => 'UTF-8',
1402         }
1403     ) or die Template->error();
1404
1405     my $tt_params = _get_tt_params( $tables );
1406
1407     my $output;
1408     $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1409
1410     return $output;
1411 }
1412
1413 sub _get_tt_params {
1414     my ($tables) = @_;
1415
1416     my $params;
1417
1418     my $config = {
1419         biblio => {
1420             module   => 'Koha::Biblios',
1421             singular => 'biblio',
1422             plural   => 'biblios',
1423             pk       => 'biblionumber',
1424         },
1425         borrowers => {
1426             module   => 'Koha::Patrons',
1427             singular => 'borrower',
1428             plural   => 'borrowers',
1429             pk       => 'borrowernumber',
1430         },
1431         branches => {
1432             module   => 'Koha::Libraries',
1433             singular => 'branch',
1434             plural   => 'branches',
1435             pk       => 'branchcode',
1436         },
1437         items => {
1438             module   => 'Koha::Items',
1439             singular => 'item',
1440             plural   => 'items',
1441             pk       => 'itemnumber',
1442         },
1443         opac_news => {
1444             module   => 'Koha::News',
1445             singular => 'news',
1446             plural   => 'news',
1447             pk       => 'idnew',
1448         },
1449         reserves => {
1450             module   => 'Koha::Holds',
1451             singular => 'hold',
1452             plural   => 'holds',
1453             fk       => [ 'borrowernumber', 'biblionumber' ],
1454         },
1455         serial => {
1456             module   => 'Koha::Serials',
1457             singular => 'serial',
1458             plural   => 'serials',
1459             pk       => 'serialid',
1460         },
1461         subscription => {
1462             module   => 'Koha::Subscriptions',
1463             singular => 'subscription',
1464             plural   => 'subscriptions',
1465             pk       => 'subscriptionid',
1466         },
1467         suggestions => {
1468             module   => 'Koha::Suggestions',
1469             singular => 'suggestion',
1470             plural   => 'suggestions',
1471             pk       => 'suggestionid',
1472         },
1473         issues => {
1474             module   => 'Koha::Checkouts',
1475             singular => 'checkout',
1476             plural   => 'checkouts',
1477             fk       => 'itemnumber',
1478         },
1479         borrower_modifications => {
1480             module   => 'Koha::Patron::Modifications',
1481             singular => 'patron_modification',
1482             plural   => 'patron_modifications',
1483             fk       => 'verification_token',
1484         },
1485     };
1486
1487     foreach my $table ( keys %$tables ) {
1488         next unless $config->{$table};
1489
1490         my $ref = ref( $tables->{$table} ) || q{};
1491         my $module = $config->{$table}->{module};
1492
1493         if ( can_load( modules => { $module => undef } ) ) {
1494             my $pk = $config->{$table}->{pk};
1495             my $fk = $config->{$table}->{fk};
1496
1497             if ( $ref eq q{} || $ref eq 'HASH' ) {
1498                 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1499                 my $object;
1500                 if ( $fk ) { # Using a foreign key for lookup
1501                     if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1502                         my $search;
1503                         foreach my $key ( @$fk ) {
1504                             $search->{$key} = $id->{$key};
1505                         }
1506                         $object = $module->search( $search )->next();
1507                     } else { # Foreign key is single column
1508                         $object = $module->search( { $fk => $id } )->next();
1509                     }
1510                 } else { # using the table's primary key for lookup
1511                     $object = $module->find($id);
1512                 }
1513                 $params->{ $config->{$table}->{singular} } = $object;
1514             }
1515             else {    # $ref eq 'ARRAY'
1516                 my $object;
1517                 if ( @{ $tables->{$table} } == 1 ) {    # Param is a single key
1518                     $object = $module->search( { $pk => $tables->{$table} } )->next();
1519                 }
1520                 else {                                  # Params are mutliple foreign keys
1521                     my @values = @{ $tables->{$table} };
1522                     my @keys   = @{ $config->{$table}->{fk} };
1523                     my %params = map { $_ => shift(@values) } @keys;
1524                     $object = $module->search( \%params )->next();
1525                 }
1526                 $params->{ $config->{$table}->{singular} } = $object;
1527             }
1528         }
1529         else {
1530             croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1531         }
1532     }
1533
1534     $params->{today} = dt_from_string();
1535
1536     return $params;
1537 }
1538
1539
1540 1;
1541 __END__