From 84f62e1bab8b75de484c734391e535a32dfef390 Mon Sep 17 00:00:00 2001
From: blou
Date: Mon, 7 Oct 2013 09:38:26 -0400
Subject: [PATCH 1/1] Bug 8753 - Add forgot password link to OPAC
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Sponsor: Institut universitaire en santé mentale de Montréal
TEST PLAN:
1) apply the patch
2) go to system preferences OPAC>>Privacy and set 'OpacResetPassword' to ON. That will cause the link 'Forgot your password' to show up on the welcome page, below connection box.
3) refresh front page, click on 'Forgot your password' and enter a VALID address (one that is associated to an entry in borrowers.email or borrowers.email_pro.
3b) Also try an INVALID address (valid yet not in your koha db). An error message will show up.
4) An email should be received at that address with a link.
5) Follow the link in the mail to fill the new password.
Until a satisfactory new password is entered, the old password is not reset.
6) Go to main page try the new password.
http://bugs.koha-community.org/show_bug.cgi?id=10951
---
C4/Auth.pm | 2 +
C4/Members.pm | 82 +++++++-
installer/data/mysql/kohastructure.sql | 11 +
installer/data/mysql/sysprefs.sql | 3 +-
installer/data/mysql/updatedatabase.pl | 8 +
.../prog/en/modules/admin/preferences/opac.pref | 7 +
koha-tmpl/opac-tmpl/prog/en/modules/opac-auth.tt | 3 +
koha-tmpl/opac-tmpl/prog/en/modules/opac-main.tt | 6 +-
.../prog/en/modules/opac-password-recovery.tt | 129 ++++++++++++
.../prog/en/modules/opac-send-password-recovery.tt | 13 ++
opac/opac-main.pl | 1 +
opac/opac-password-recovery.pl | 223 +++++++++++++++++++++
12 files changed, 485 insertions(+), 3 deletions(-)
create mode 100644 koha-tmpl/opac-tmpl/prog/en/modules/opac-password-recovery.tt
create mode 100644 koha-tmpl/opac-tmpl/prog/en/modules/opac-send-password-recovery.tt
create mode 100755 opac/opac-password-recovery.pl
diff --git a/C4/Auth.pm b/C4/Auth.pm
index d73edaf..f702011 100644
--- a/C4/Auth.pm
+++ b/C4/Auth.pm
@@ -1043,6 +1043,8 @@ sub checkauth {
PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
persona => C4::Context->preference("Persona"),
opac_css_override => $ENV{'OPAC_CSS_OVERRIDE'},
+ OpacResetPassword => C4::Context->preference("OpacResetPassword"),
+ OpacPasswordChange => C4::Context->preference("OpacPasswordChange"),
);
$template->param( OpacPublic => C4::Context->preference("OpacPublic"));
diff --git a/C4/Members.pm b/C4/Members.pm
index 62a07dd..2b6a091 100644
--- a/C4/Members.pm
+++ b/C4/Members.pm
@@ -41,6 +41,12 @@ use Koha::DateUtils;
use Text::Unaccent qw( unac_string );
use Koha::AuthUtils qw(hash_password);
+## Password recovery
+use CGI;
+use Encode qw(encode decode);
+use MIME::QuotedPrint qw(encode_qp);
+use Mail::Sendmail;
+
our ($VERSION,@ISA,@EXPORT,@EXPORT_OK,$debug);
BEGIN {
@@ -109,7 +115,12 @@ BEGIN {
push @EXPORT, qw(
&ModMember
&changepassword
- &ModPrivacy
+ &ModPrivacy
+ );
+
+ # Password recovery
+ push @EXPORT, qw(
+ &SendPasswordRecoveryEmail
);
#Delete data
@@ -2264,6 +2275,75 @@ sub ModPrivacy {
privacy => $privacy );
}
+
+sub SendPasswordRecoveryEmail {
+ my $borrowernumber = shift;
+ my $email = shift;
+ my $dbh = C4::Context->dbh;
+ #use constant EMAIL_ENCODE = 'iso-8859-1';
+ my $emailEncode = 'iso-8859-1';
+ my $username = GetMemberDetails($borrowernumber)->{userid};
+
+ #generate UUID
+ my @chars = ("A".."Z", "a".."z", "0".."9");
+ my $uuid_str;
+ $uuid_str .= $chars[rand @chars] for 1..32;
+
+ #insert into database
+ my $sth = $dbh->prepare( 'INSERT INTO borrower_password_recovery VALUES (? ,? , ADDDATE(NOW(), INTERVAL 2 DAY) )' );
+ $sth->execute($borrowernumber, $uuid_str);
+
+ my $userEmail = ( $email ) ? $email : GetFirstValidEmailAddress($borrowernumber);
+ #define to/from emails
+ my $kohaEmail = C4::Context->preference( 'KohaAdminEmailAddress' );
+
+ #create link
+ my $uuidLink = "http://" . C4::Context->preference( 'OPACBaseURL' ) . "/cgi-bin/koha/opac-password-recovery.pl?uniqueKey=$uuid_str";
+ #warn $uuidLink;
+
+ #build email content
+ my $query = new CGI;
+ my ( $template2, $borrower_number, $cookie ) = C4::Auth::get_template_and_user(
+ {
+ template_name => "opac-send-password-recovery.tmpl",
+ query => $query,
+ type => "opac",
+ authnotrequired => 1,
+ }
+ );
+ $template2->param(
+ uuidLink => $uuidLink,
+ username => $username,
+ );
+
+ # Getting template result and
+ my $template_res = $template2->output();
+
+ #Mail attributes
+ my %mail = (
+ To => $userEmail,
+ From => $kohaEmail,
+ 'Content-Type' => 'text/html; charset="' . $emailEncode . '"',
+ );
+
+ #getting mail properties
+ if ( $template_res =~ /(.*)/s ) { $mail{'subject'} = decode( 'utf-8', $1 ); }
+ if ( $template_res =~ /\n(.*)\n/s ) { $mail{'body'} = decode( 'utf-8', $1 ); }
+ #send mail
+ if ( sendmail %mail )
+ {
+ # if it works....
+ return 1;
+ }
+ else
+ {
+ # if it doesnt work....
+ warn "Error sending mail: $Mail::Sendmail::error \n";
+ return 0;
+ }
+}
+
+
=head2 AddMessage
AddMessage( $borrowernumber, $message_type, $message, $branchcode );
diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql
index e1e3e96..f56e2ed 100644
--- a/installer/data/mysql/kohastructure.sql
+++ b/installer/data/mysql/kohastructure.sql
@@ -3275,6 +3275,17 @@ ALTER TABLE `patron_list_patrons`
ADD CONSTRAINT patron_list_patrons_ibfk_1 FOREIGN KEY (patron_list_id) REFERENCES patron_lists (patron_list_id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT patron_list_patrons_ibfk_2 FOREIGN KEY (borrowernumber) REFERENCES borrowers (borrowernumber) ON DELETE CASCADE ON UPDATE CASCADE;
+--
+-- Table structure for table 'borrower_password_recovery'
+-- this stores the unique ID sent by email to the patron, for future validation
+--
+
+CREATE TABLE IF NOT EXISTS `borrower_password_recovery` (
+ `borrowernumber` int(11) NOT NULL,
+ `uuid` varchar(128) NOT NULL,
+ `valid_until` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ KEY `borrowernumber` (`borrowernumber`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql
index 71513ce..5dafa2c 100644
--- a/installer/data/mysql/sysprefs.sql
+++ b/installer/data/mysql/sysprefs.sql
@@ -253,6 +253,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
('opacreadinghistory','1','','If ON, enables display of Patron Circulation History in OPAC','YesNo'),
('OpacRenewalAllowed','0',NULL,'If ON, users can renew their issues directly from their OPAC account','YesNo'),
('OpacRenewalBranch','checkoutbranch','itemhomebranch|patronhomebranch|checkoutbranch|null','Choose how the branch for an OPAC renewal is recorded in statistics','Choice'),
+('OpacResetPassword','1','','Shows the \'Forgot your password?\' link in the OPAC','YesNo'),
('OPACResultsSidebar','','70|10','Define HTML to be included on the search results page, underneath the facets sidebar','Textarea'),
('OPACSearchForTitleIn','Other Libraries (WorldCat) \nOther Databases (Google Scholar) \nOnline Stores (Bookfinder.com) \nOpen Library (openlibrary.org) ','70|10','Enter the HTML that will appear in the \'Search for this title in\' box on the detail page in the OPAC. Enter {TITLE}, {AUTHOR}, or {ISBN} in place of their respective variables in the URL. Leave blank to disable \'More Searches\' menu.','Textarea'),
('OpacSeparateHoldings','0',NULL,'Separate current branch holdings from other holdings (OPAC)','YesNo'),
@@ -419,5 +420,5 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'),
('yuipath','local','local|http://yui.yahooapis.com/2.5.1/build','Insert the path to YUI libraries, choose local if you use koha offline','Choice'),
('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'),
-('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo')
+('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo'),
;
diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl
index 9f57eff..556911b 100755
--- a/installer/data/mysql/updatedatabase.pl
+++ b/installer/data/mysql/updatedatabase.pl
@@ -7306,6 +7306,14 @@ if ( CheckVersion($DBversion) ) {
SetVersion($DBversion);
}
+$DBversion = "3.13.00.XXX";
+if ( CheckVersion($DBversion) ) {
+ $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type)
+VALUES('OpacResetPassword','1','Shows the ''Forgot your password?'' link in the OPAC','','YesNo');");
+ print "Upgrade to $DBversion done (Bug 8753: Add forgot password link to OPAC)\n";
+ SetVersion($DBversion);
+}
+
=head1 FUNCTIONS
=head2 TableExists($table)
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
index a75c7ef..2bfc05d 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
@@ -551,6 +551,13 @@ OPAC:
track: "Track"
no: "Don't track"
- links that patrons click on
+ -
+ - pref: OpacResetPassword
+ default: 1
+ choices:
+ yes: "On"
+ no: "Off"
+ - ". If On, the user can reset his password on OPAC."
Shelf Browser:
-
diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/opac-auth.tt b/koha-tmpl/opac-tmpl/prog/en/modules/opac-auth.tt
index 7b8b8f9..e0d2974 100644
--- a/koha-tmpl/opac-tmpl/prog/en/modules/opac-auth.tt
+++ b/koha-tmpl/opac-tmpl/prog/en/modules/opac-auth.tt
@@ -85,6 +85,9 @@ please choose against which one you would like to authenticate:
+[% IF OpacPasswordChange && OpacResetPassword %]
+ Forgot your password?
+[% END %]
Don't have a password yet? If you don't have a password yet, stop by the circulation desk the next time you're in the library. We'll happily set one up for you.
Don't have a library card? If you don't have a library card, stop by your local library to sign up[% IF PatronSelfRegistration && PatronSelfRegistrationDefaultCategory %] or register here [% END %].
diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/opac-main.tt b/koha-tmpl/opac-tmpl/prog/en/modules/opac-main.tt
index eb1e600..423ce0c 100644
--- a/koha-tmpl/opac-tmpl/prog/en/modules/opac-main.tt
+++ b/koha-tmpl/opac-tmpl/prog/en/modules/opac-main.tt
@@ -60,7 +60,11 @@
[% IF PatronSelfRegistration && PatronSelfRegistrationDefaultCategory %]
[% END %]
-
+
+ [% IF OpacPasswordChange && OpacResetPassword %]
+
Forgot your password?
+ [% END %]
+
[% END %]
diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/opac-password-recovery.tt b/koha-tmpl/opac-tmpl/prog/en/modules/opac-password-recovery.tt
new file mode 100644
index 0000000..9055e0d
--- /dev/null
+++ b/koha-tmpl/opac-tmpl/prog/en/modules/opac-password-recovery.tt
@@ -0,0 +1,129 @@
+[% INCLUDE 'doc-head-open.inc' %]
+[% IF (LibraryNameTitle) %][% LibraryNameTitle %][% ELSE %]Koha Online[% END %] Catalog ›
+[% INCLUDE 'doc-head-close.inc' %]
+
+
+
+
+
+
+
+[% INCLUDE 'masthead.inc' %]
+
+
+
+
+[% IF (!OpacResetPassword) %]
+
You can't reset your password.
+[% ELSIF (password_recovery) %]
+ [% IF (hasError) %]
+
+ [% IF (sendmailError) %]
+ An error has occured while sending you the password recovery link.
+ Please try again later.
+ [% ELSIF (errNoEmailFound) %]
+ No account was found with the email address "[% email %] "
+ Check if you typed it correctly.
+ [% ELSIF (errTooManyEmailFound) %]
+ More than one account has been found for the email address: "[% email %] "
+ Try yo use your alternative email if you have another.
+ [% ELSIF (errAlreadyStartRecovery) %]
+ The process of password recovery has already started for this account ("[% email %] ")
+ Check your emails; you should recieve the link to reset your password.
+ If you didn't recieve it, click here to get a new password recovery link
+ [% END %]
+ Please contact the staff if you need further assistance.
+
+ [% END %]
+
+
+[% ELSIF (new_password) %]
+ [% IF (errLinkNotValid) %]
+
+ We could not authentify you as the account owner.
+ Be sure to use the link you recieved in your email.
+
+ [% ELSE %]
+ [% IF (hasError) %]
+
+ [% IF (errPassNotMatch) %]
+ The passwords entered does not match.
+ Please try again.
+ [% ELSIF (errPassTooShort) %]
+ The password is too short.
+ The password must contain at least [% minPassLength %] characters.
+ [% END %]
+
+ [% END %]
+
+ [% END %]
+[% ELSIF (mail_sent) %]
+
A mail has been sent to "[% email %]".
+ It contains a link to create a new password.
+ This link will be valid for 2 days from now.
+
Click here to return to the main page.
+[% ELSIF (password_reset_done) %]
+
The password has been changed for the user "[% username %]".
+ You can now login using this form .
+[% END %]
+
+
+
+
+
+
+ [% INCLUDE 'usermenu.inc' %]
+
+
+
+[% INCLUDE 'opac-bottom.inc' %]
+
diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/opac-send-password-recovery.tt b/koha-tmpl/opac-tmpl/prog/en/modules/opac-send-password-recovery.tt
new file mode 100644
index 0000000..19d12a4
--- /dev/null
+++ b/koha-tmpl/opac-tmpl/prog/en/modules/opac-send-password-recovery.tt
@@ -0,0 +1,13 @@
+[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha Online[% END %] - Password recovery
+
+
+
+This email has been sent in response to your password recovery request for the account [% username %] .
+
+You can now create your new password using the following link:
+[% uuidLink %]
+
+This link will be valid for 2 days from this email's reception, then you must reapply if you do not change your password.
+Thank you.
+
+
diff --git a/opac/opac-main.pl b/opac/opac-main.pl
index 7c6ee36..15dc8cd 100755
--- a/opac/opac-main.pl
+++ b/opac/opac-main.pl
@@ -58,6 +58,7 @@ $template->param(
koha_news_count => $koha_news_count,
display_daily_quote => C4::Context->preference('QuoteOfTheDay'),
daily_quote => $quote,
+ OpacResetPassword => C4::Context->preference('OpacResetPassword'),
);
# If GoogleIndicTransliteration system preference is On Set paramter to load Google's javascript in OPAC search screens
diff --git a/opac/opac-password-recovery.pl b/opac/opac-password-recovery.pl
new file mode 100755
index 0000000..9ddc641
--- /dev/null
+++ b/opac/opac-password-recovery.pl
@@ -0,0 +1,223 @@
+#!/usr/bin/perl
+
+use strict;
+
+use CGI;
+use Mail::Sendmail;
+use Digest::MD5 qw(md5_base64);
+use HTML::Entities;
+
+use C4::Auth;
+use C4::Koha;
+use C4::Members qw(changepassword GetMember GetMemberDetails SendPasswordRecoveryEmail);
+use C4::Output;
+use C4::Context;
+
+my $query = new CGI;
+
+my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
+ {
+ template_name => "opac-password-recovery.tmpl",
+ query => $query,
+ type => "opac",
+ authnotrequired => 1,
+ debug => 1,
+ }
+);
+
+my $email = $query->param('email');
+my $password = $query->param('password');
+my $repeatPassword = $query->param('repeatPassword');
+my $minPassLength = C4::Context->preference( 'minPasswordLength' );
+my $id = $query->param('id');
+my $uniqueKey = $query->param('uniqueKey');
+my $username = $query->param('username');
+my $borrower_number;
+
+#errors
+my $hasError;
+
+#email form error
+my $errNoEmailFound;
+my $errAlreadyStartRecovery;
+
+#new password form error
+my $errLinkNotValid;
+my $errPassNotMatch;
+my $errPassTooShort;
+
+my $dbh = C4::Context->dbh;
+
+$template->param( OpacResetPassword => C4::Context->preference("OpacResetPassword") );
+
+if ( $query->param('sendEmail') || $query->param('resendEmail') )
+{
+#send mail + confirmation
+
+ #try with the main email
+ my $borrower_number;
+ my $accountActivated;
+ my %borrower_infos = GetMember(email => $email);
+ if ( %borrower_infos )
+ {
+ $borrower_number = GetMember(email => $email)->{'borrowernumber'};
+ $username = GetMemberDetails( $borrower_number )->{'userid'};
+ $accountActivated = ( GetMemberDetails( $borrower_number )->{'categorycode'} ne 'UNAUTHORIZ' ) ? 1 : 0;
+ }
+ else
+ {
+ #try with the secondary email
+ %borrower_infos = GetMember(emailpro => $email);
+ if ( %borrower_infos )
+ {
+ $borrower_number = GetMember(emailpro => $email)->{'borrowernumber'};
+ $username = GetMemberDetails( $borrower_number )->{'userid'};
+ $accountActivated = ( GetMemberDetails( $borrower_number )->{'categorycode'} ne 'UNAUTHORIZ' ) ? 1 : 0;
+ }
+ else
+ {
+ #try when the other contact email
+ %borrower_infos = GetMember(B_email => $email);
+ if ( %borrower_infos )
+ {
+ $borrower_number = GetMember(B_email => $email)->{'borrowernumber'};
+ $username = GetMemberDetails( $borrower_number )->{'userid'};
+ $accountActivated = ( GetMemberDetails( $borrower_number )->{'categorycode'} ne 'UNAUTHORIZ' ) ? 1 : 0;
+ }
+ }
+ }
+
+ if ( !$email || !$username || !$borrower_number || !$accountActivated )
+ {
+ $hasError = 1;
+ $errNoEmailFound = 1;
+ }
+ elsif ( !$query->param('resendEmail') )
+ {
+ my $sth = $dbh->prepare( "SELECT borrowernumber FROM borrower_password_recovery WHERE NOW() < valid_until AND borrowernumber = ?" );
+ $sth->execute($borrower_number);
+ if ( my $already = $sth->fetchrow )
+ {
+ $hasError = 1;
+ $errAlreadyStartRecovery = 1;
+ }
+ }
+
+ if ( $hasError )
+ {
+ $template->param( hasError => 1,
+ errNoEmailFound => $errNoEmailFound,
+ errAlreadyStartRecovery => $errAlreadyStartRecovery,
+ password_recovery => 1,
+ email => HTML::Entities::encode($email),
+ );
+ }
+ else
+ {
+ #generate uuid and send recovery email
+ if ( SendPasswordRecoveryEmail($borrower_number, $email) )
+ {
+ # if it works....
+ $template->param( mail_sent => 1,
+ email => $email
+ );
+ }
+ else
+ {
+ # if it doesnt work....
+ $template->param( password_recovery => 1,
+ sendmailError => 1
+ );
+ }
+ }
+}
+elsif ( $query->param('passwordReset') )
+{
+#new password form
+ #check if the link is still valid
+ my $sth = $dbh->prepare( "SELECT borrower_password_recovery.borrowernumber, userid
+ FROM borrower_password_recovery, borrowers
+ WHERE borrowers.borrowernumber = borrower_password_recovery.borrowernumber
+ AND NOW() < valid_until
+ AND uuid = ?" );
+ $sth->execute( $uniqueKey );
+ ( $borrower_number , $username) = $sth->fetchrow;
+
+ #validate password length & match
+ if ( ( $borrower_number ) && ( $password eq $repeatPassword ) && ( length( $password ) >= $minPassLength ) )
+ {
+ #apply changes
+ changepassword( $username, $borrower_number, md5_base64($password) );
+ #this line needed to fix a log bug in "Log.pm:75"
+ #
+ # if ( !$usernumber ) { $usernumber=0; }
+ #
+
+ #remove entry
+ my $sth = $dbh->prepare( "DELETE FROM borrower_password_recovery
+ WHERE uuid = ?
+ ORDER BY valid_until DESC LIMIT 1" );
+ $sth->execute( $uniqueKey);
+
+ $template->param( password_reset_done => 1,
+ username => $username
+ );
+ }
+ else
+ {
+ #errors
+ if ( !$borrower_number )
+ {
+ #parameters not valid
+ $errLinkNotValid = 1;
+ }
+ elsif ( $password ne $repeatPassword )
+ {
+ #passwords does not match
+ $errPassNotMatch = 1;
+ }
+ elsif ( length( $password ) < $minPassLength )
+ {
+ #password too short
+ $errPassTooShort = 1;
+ }
+ $template->param( new_password => 1,
+ minPassLength => $minPassLength,
+ email => $email,
+ uniqueKey => $uniqueKey,
+ errLinkNotValid => $errLinkNotValid,
+ errPassNotMatch => $errPassNotMatch,
+ errPassTooShort => $errPassTooShort,
+ hasError => 1 );
+ }
+}
+elsif ( $uniqueKey )
+{
+#reset password form
+ #check if the link is valid
+ my $sth = $dbh->prepare( "SELECT borrower_password_recovery.borrowernumber, userid
+ FROM borrower_password_recovery, borrowers
+ WHERE borrowers.borrowernumber = borrower_password_recovery.borrowernumber
+ AND NOW() < valid_until
+ AND uuid = ?" );
+ $sth->execute( $uniqueKey );
+ ( $borrower_number, $username ) = $sth->fetchrow;
+ if( !$borrower_number )
+ {
+ $errLinkNotValid = 1;
+ }
+ $template->param( new_password => 1,
+ minPassLength => $minPassLength,
+ email => $email,
+ uniqueKey => $uniqueKey,
+ username => $username,
+ errLinkNotValid => $errLinkNotValid
+ );
+}
+else
+{
+ #password recovery form (to send email)
+ $template->param( password_recovery => 1 );
+}
+
+output_html_with_http_headers $query, $cookie, $template->output;
--
1.8.4.rc3