@@ -, +, @@ --- Koha/Validation.pm | 138 ++++++++++++++++ .../bug_14620_email_and_phone_validations.sql | 2 + installer/data/mysql/sysprefs.sql | 2 + .../prog/en/modules/admin/preferences/patrons.pref | 16 ++ .../prog/en/modules/members/memberentrygen.tt | 67 ++++++++ .../bootstrap/en/modules/opac-memberentry.tt | 49 ++++++ .../bootstrap/en/modules/opac-messaging.tt | 56 +++++++ .../bootstrap/lib/bootstrap/css/bootstrap.css | 24 +++ .../bootstrap/lib/bootstrap/css/bootstrap.min.css | 2 +- members/memberentry.pl | 38 +++-- opac/opac-memberentry.pl | 25 ++- opac/opac-messaging.pl | 13 +- t/db_dependent/Validations.t | 184 +++++++++++++++++++++ 13 files changed, 594 insertions(+), 22 deletions(-) create mode 100644 Koha/Validation.pm create mode 100644 installer/data/mysql/atomicupdate/bug_14620_email_and_phone_validations.sql create mode 100644 t/db_dependent/Validations.t --- a/Koha/Validation.pm +++ a/Koha/Validation.pm @@ -0,0 +1,138 @@ +package Koha::Validation; + +# 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 strict; +use warnings; + +use C4::Context; +use Email::Valid; + +use vars qw($VERSION); + +=head1 NAME + +Koha::Validation - validates inputs + +=head1 SYNOPSIS + + use Koha::Validation + +=head1 DESCRIPTION + +This module lets you validate given inputs. + +=head2 validate_email + +Validates given email. + + Koha::Validation::validate_email("email@address.com"); + +returns: 1 if the given email is valid, 0 otherwise. + +=cut + +sub validate_email { + + my $address = shift; + + # make sure we are allowed to validate emails + return 1 if not C4::Context->preference("ValidateEmailAddress"); + return 1 if not $address; + return 0 if $address =~ /(^(\s))|((\s)$)/; + return (not defined Email::Valid->address($address)) ? 0:1; +} + +=head2 validate_phonenumber + +Validates given phone number. + + Koha::Validation::validate_phonenumber(123456789); + +returns: 1 if the given phone number is valid, 0 otherwise. + +=cut + +sub validate_phonenumber { + my $phonenumber = shift; + + # make sure we are allowed to validate phone numbers + return 1 if C4::Context->preference("ValidatePhoneNumber") eq "OFF"; + return 1 if not $phonenumber; + return 0 if $phonenumber =~ /(^(\s))|((\s)$)/; + + my $regex = get_phonenumber_regex(C4::Context->preference("ValidatePhoneNumber")); + return ($phonenumber !~ /$regex/) ? 0:1; +} + +=head2 get_phonenumber_regex + +Returns the used regex (according to ValidatePhoneNumber system preference) for phone numbers. +This is used to share the same regex between Perl scripts and JavaScript in templates. + + Koha::Validation::get_phonenumber_regex(); + +International phone numbers (ipn): http://regexlib.com/REDetails.aspx?regexp_id=3009 + +returns: the regex + +=cut + +sub get_phonenumber_regex { + if (C4::Context->preference("ValidatePhoneNumber") eq "ipn") { + return qr/^((\+)?[1-9]{1,2})?([-\s\.])?((\(\d{1,4}\))|\d{1,4})(([-\s\.])?[0-9]{1,12}){1,2}$/; + } + elsif (C4::Context->preference("ValidatePhoneNumber") eq "fin") { + return qr/^((90[0-9]{3})?0|\+358\s?)(?!(100|20(0|2(0|[2-3])|9[8-9])|300|600|700|708|75(00[0-3]|(1|2)\d{2}|30[0-2]|32[0-2]|75[0-2]|98[0-2])))(4|50|10[1-9]|20(1|2(1|[4-9])|[3-9])|29|30[1-9]|71|73|75(00[3-9]|30[3-9]|32[3-9]|53[3-9]|83[3-9])|2|3|5|6|8|9|1[3-9])\s?(\d\s?){4,19}\d$/; + } + + return ""; + } + + +=head2 use_validator + +Validates given input with given validator. + + Koha::Validation::use_validator("phone", 123456789); + Koha::Validation::use_validator("email", "email@address.com"); + +Currently supported validators are + email, e-mail + phone, phonenumber + +returns: 1 if the given input is valid with the given validator, 0 otherwise. + +=cut + +sub use_validator { + my ($validator, $input) = @_; + + if (not defined $validator or not defined $input) { + warn "Subroutine must be called with validator and input"; + return 0; + } + + return validate_email($input) if $validator eq "email" or $validator eq "e-mail"; + return validate_phonenumber($input) if $validator eq "phone" or $validator eq "phonenumber"; + + warn "Validator not found"; + return 0; +} + +1; --- a/installer/data/mysql/atomicupdate/bug_14620_email_and_phone_validations.sql +++ a/installer/data/mysql/atomicupdate/bug_14620_email_and_phone_validations.sql @@ -0,0 +1,2 @@ +INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES ('ValidatePhoneNumber','OFF','ipn|fin|new|OFF','Validation of phone number on patrons.','Choice'); +INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES ('ValidateEmailAddress','0','','Validation of email address on patrons.','YesNo'); --- a/installer/data/mysql/sysprefs.sql +++ a/installer/data/mysql/sysprefs.sql @@ -475,6 +475,8 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('UseQueryParser','0',NULL,'If enabled, try to use QueryParser for queries.','YesNo'), ('UseTransportCostMatrix','0','','Use Transport Cost Matrix when filling holds','YesNo'), ('UseWYSIWYGinSystemPreferences','0','','Show WYSIWYG editor when editing certain HTML system preferences.','YesNo'), +('ValidateEmailAddress','0','','Validation of email address on patrons.','YesNo'), +('ValidatePhoneNumber','OFF','ipn|fin|new|OFF','Validation of phone number on patrons.','Choice'), ('viewISBD','1','','Allow display of ISBD view of bibiographic records','YesNo'), ('viewLabeledMARC','0','','Allow display of labeled MARC view of bibiographic records','YesNo'), ('viewMARC','1','','Allow display of MARC view of bibiographic records','YesNo'), --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref @@ -138,6 +138,22 @@ Patrons: yes: Allow no: "Don't allow" - librarians to discharge borrowers and borrowers to request a discharge. + - + - Use validator + - pref: ValidatePhoneNumber + choices: + ipn: International Phone Numbers + fin: Finnish Phone Numbers + new: ...Not yet implemented... + "OFF": none + - to validate phone numbers on patrons. + - + - pref: ValidateEmailAddress + choices: + yes: Enable + no: Disable + - validation of email addresses on patrons. + "Norwegian patron database": - - pref: NorwegianPatronDBEnable --- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt @@ -34,8 +34,39 @@ } [% END %] $("#dateofbirth").datepicker({ maxDate: "-1D" }); + + var MSG_INCORRECT_PHONE = _("Please enter a valid phone number."); + $.validator.addMethod('phone', function(value) { + value = value.trim(); + if (!value.trim()) { + return 1; + } + else { + return (value.match(/[% phone_regex %]/)); + } + }, + MSG_INCORRECT_PHONE); + $("#entryform").validate({ rules: { + [% IF ( ValidatePhoneNumber ) %] + phone: { + phone: true + }, + phonepro: { + phone: true + }, + mobile: { + phone: true + }, + SMSnumber: { + phone: true + }, + B_phone: { + phone: true + }[% IF ( ValidateEmailAddress ) %],[% END %] + [% END %] + [% IF ( ValidateEmailAddress ) %] email: { email: true }, @@ -45,6 +76,7 @@ B_email: { email: true } + [% END %] }, submitHandler: function(form) { $("body, form input[type='submit'], form button[type='submit'], form a").addClass('waiting'); @@ -52,10 +84,33 @@ return false; else form.beenSubmitted = true; + [% IF ValidateEmailAddress %] + $("#email, #emailpro, #B_email").each(function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] + [% IF ValidatePhoneNumber %] + $("#phone, #phonepro, #B_phone, #SMSnumber").each(function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] form.submit(); } }); + [% IF ValidateEmailAddress %] + $("#email, #emailpro, #B_email").on("change", function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] + [% IF ValidatePhoneNumber %] + $("#SMSnumber").on("change", function(){ + $(this).val($.trim($(this).val())); + }); + $("#phone, #phonepro, #B_phone").on("change", function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] var mrform = $("#manual_restriction_form"); var mrlink = $("#add_manual_restriction"); mrform.hide(); @@ -254,6 +309,18 @@ [% IF ERROR_bad_email_alternative %]
  • The alternative email is invalid.
  • [% END %] + [% IF ERROR_bad_phone %] +
  • The primary phone is invalid.
  • + [% END %] + [% IF ERROR_bad_phone_secondary %] +
  • The secondary phone is invalid.
  • + [% END %] + [% IF ERROR_bad_phone_alternative %] +
  • The alternative phone is invalid.
  • + [% END %] + [% IF ERROR_bad_smsnumber %] +
  • The SMS number is invalid.
  • + [% END %] [% END %] --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt +++ a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt @@ -60,6 +60,10 @@ [% IF field == "email" %]
  • Contact information: primary email address
  • [% END %] [% IF field == "emailpro" %]
  • Contact information: secondary email address
  • [% END %] [% IF field == "B_email" %]
  • Alternate address information: email address
  • [% END %] + [% IF field == "phone" %]
  • Contact information: primary phone
  • [% END %] + [% IF field == "phonepro" %]
  • Contact information: secondary phone
  • [% END %] + [% IF field == "mobile" %]
  • Contact information: other phone
  • [% END %] + [% IF field == "B_phone" %]
  • Alternate address information: phone
  • [% END %] [% END %] Please correct the errors and resubmit. @@ -822,8 +826,19 @@ [% ELSE %] $( "#borrower_dateofbirth" ).datepicker({ yearRange: "c-120:c" }); [% END %] + var MSG_INCORRECT_PHONE = _("Please enter a valid phone number."); + $.validator.addMethod('phone', function(value) { + if (!value.trim()) { + return 1; + } + else { + return (value.match(/[% phone_regex %]/)); + } + }, + MSG_INCORRECT_PHONE); $("#memberentry-form").validate({ rules: { + [% IF ( ValidateEmailAddress ) %] borrower_email: { email: true }, @@ -832,7 +847,25 @@ }, borrower_B_email: { email: true + }[% IF ( ValidatePhoneNumber ) %],[% END %] + [% END %] + [% IF ( ValidatePhoneNumber ) %] + borrower_phone: { + phone: true + }, + borrower_phonepro: { + phone: true + }, + borrower_mobile: { + phone: true + }, + SMSnumber: { + phone: true + }, + borrower_B_phone: { + phone: true } + [% END %] }, submitHandler: function(form) { if (form.beenSubmitted) { @@ -840,6 +873,16 @@ } else { form.beenSubmitted = true; + [% IF ValidateEmailAddress %] + $("#borrower_email, #borrower_emailpro, #borrower_B_email").each(function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] + [% IF ValidatePhoneNumber %] + $("#borrower_phone, #borrower_phonepro, #borrower_B_phone, #SMSnumber").each(function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] form.submit(); } }, @@ -853,6 +896,12 @@ error.css('width', 'auto'); } }); + $("#borrower_email, #borrower_emailpro, #borrower_B_email").on("change", function(){ + $(this).val($.trim($(this).val())); + }); + $("#borrower_phone, #borrower_phonepro, #borrower_B_phone").on("change", function(){ + $(this).val($.trim($(this).val())); + }); }); //]]> --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-messaging.tt +++ a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-messaging.tt @@ -149,7 +149,63 @@ } }); $("#info_digests").tooltip(); + var MSG_INCORRECT_PHONE = _("Please enter a valid phone number."); + + $("fieldset.rows input, fieldset.rows select").addClass("noEnterSubmit"); + + var MSG_INCORRECT_PHONE = _("Please enter a valid phone number."); + $.validator.addMethod('phone', function(value) { + if (!value.trim()) { + return 1; + } + else { + return (value.match(/[% phone_regex %]/)); + } + }, + MSG_INCORRECT_PHONE); + + $("form[name='opacmessaging']").validate({ + rules: { + [% IF ( ValidatePhoneNumber ) %] + SMSnumber: { + phone: true + } + [% END %] + }, + submitHandler: function(form) { + $("body, form input[type='submit'], form button[type='submit'], form a").addClass('waiting'); + if (form.beenSubmitted) { + return false; + } + else { + form.beenSubmitted = true; + [% IF ValidatePhoneNumber %] + $("#SMSnumber").each(function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] + form.submit(); + } + }, + errorPlacement: function(error, element) { + offset = element.offset(); + error.insertAfter(element) + error.addClass('error'); // add a class to the wrapper + error.css('position', 'absolute'); + error.css('left', offset.left + element.outerWidth() + 10); + error.css('top', offset.top); + error.css('width', 'auto'); + } + }); + + [% IF ( ValidatePhoneNumber ) %] + $("#SMSnumber").on("change", function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] + }); //]]> + [% END %] --- a/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.css +++ a/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.css @@ -6305,3 +6305,27 @@ button.close { overflow: visible !important; } } +.lang_icon { + padding-right: 5px; + vertical-align: baseline; + height: 11px; +} +#memberentry-form input.error, +form[name=opacmessaging] input.error { + border-color: #c00; + box-shadow: 0 1px 1px #c00 inset, 0 0 8px #c00; + color: red; outline: 0 none; +} +#memberentry-form input.error:focus, +form[name=opacmessaging] input.error:focus { + border-color: #c00; + box-shadow: 0 1px 1px #c00 inset, 0 0 8px #c00; + color: red; + outline: 0 none; +} +#memberentry-form label.error, +form[name=opacmessaging] label.error { + color: #c00; + float: none; + font-size: 90%; +} --- a/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.min.css +++ a/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.min.css @@ -870,4 +870,4 @@ button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-ap .hidden-desktop{display:none !important;} .visible-desktop{display:inherit !important;} @media (min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important ;} .visible-tablet{display:inherit !important;} .hidden-tablet{display:none !important;}}@media (max-width:767px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important;} .visible-phone{display:inherit !important;} .hidden-phone{display:none !important;}}.visible-print{display:none !important;} -@media print{.visible-print{display:inherit !important;} .hidden-print{display:none !important;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-left:-20px;margin-right:-20px;} .container-fluid{padding:0;} .dl-horizontal dt{float:none;clear:none;width:auto;text-align:left;} .dl-horizontal dd{margin-left:0;} .container{width:auto;} .row-fluid{width:100%;} .row,.thumbnails{margin-left:0;} .thumbnails>li{float:none;margin-left:0;} [class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{float:none;display:block;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .row-fluid [class*="offset"]:first-child{margin-left:0;} .input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto;} .controls-row [class*="span"]+[class*="span"]{margin-left:0;} .modal{position:fixed;top:20px;left:20px;right:20px;width:auto;margin:0;}.modal.fade{top:-100px;} .modal.fade.in{top:20px;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:20px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .media .pull-left,.media .pull-right{float:none;display:block;margin-bottom:10px;} .media-object{margin-right:0;margin-left:0;} .modal{top:10px;left:10px;right:10px;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12,textarea.span12,.uneditable-input.span12{width:710px;} input.span11,textarea.span11,.uneditable-input.span11{width:648px;} input.span10,textarea.span10,.uneditable-input.span10{width:586px;} input.span9,textarea.span9,.uneditable-input.span9{width:524px;} input.span8,textarea.span8,.uneditable-input.span8{width:462px;} input.span7,textarea.span7,.uneditable-input.span7{width:400px;} input.span6,textarea.span6,.uneditable-input.span6{width:338px;} input.span5,textarea.span5,.uneditable-input.span5{width:276px;} input.span4,textarea.span4,.uneditable-input.span4{width:214px;} input.span3,textarea.span3,.uneditable-input.span3{width:152px;} input.span2,textarea.span2,.uneditable-input.span2{width:90px;} input.span1,textarea.span1,.uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12,textarea.span12,.uneditable-input.span12{width:1156px;} input.span11,textarea.span11,.uneditable-input.span11{width:1056px;} input.span10,textarea.span10,.uneditable-input.span10{width:956px;} input.span9,textarea.span9,.uneditable-input.span9{width:856px;} input.span8,textarea.span8,.uneditable-input.span8{width:756px;} input.span7,textarea.span7,.uneditable-input.span7{width:656px;} input.span6,textarea.span6,.uneditable-input.span6{width:556px;} input.span5,textarea.span5,.uneditable-input.span5{width:456px;} input.span4,textarea.span4,.uneditable-input.span4{width:356px;} input.span3,textarea.span3,.uneditable-input.span3{width:256px;} input.span2,textarea.span2,.uneditable-input.span2{width:156px;} input.span1,textarea.span1,.uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top,.navbar-fixed-bottom{position:static;} .navbar-fixed-top{margin-bottom:20px;} .navbar-fixed-bottom{margin-top:20px;} .navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .nav-collapse{clear:both;} .nav-collapse .nav{float:none;margin:0 0 10px;} .nav-collapse .nav>li{float:none;} .nav-collapse .nav>li>a{margin-bottom:2px;} .nav-collapse .nav>.divider-vertical{display:none;} .nav-collapse .nav .nav-header{color:#777777;text-shadow:none;} .nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} .nav-collapse .dropdown-menu li+li a{margin-bottom:2px;} .nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2;} .navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999999;} .navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111111;} .nav-collapse.in .btn-group{margin-top:5px;padding:0;} .nav-collapse .dropdown-menu{position:static;top:auto;left:auto;float:none;display:none;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .nav-collapse .open>.dropdown-menu{display:block;} .nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none;} .nav-collapse .dropdown-menu .divider{display:none;} .nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none;} .nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);} .navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111111;border-bottom-color:#111111;} .navbar .nav-collapse .nav.pull-right{float:none;margin-left:0;} .nav-collapse,.nav-collapse.collapse{overflow:hidden;height:0;} .navbar .btn-navbar{display:block;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}} +@media print{.visible-print{display:inherit !important;} .hidden-print{display:none !important;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-left:-20px;margin-right:-20px;} .container-fluid{padding:0;} .dl-horizontal dt{float:none;clear:none;width:auto;text-align:left;} .dl-horizontal dd{margin-left:0;} .container{width:auto;} .row-fluid{width:100%;} .row,.thumbnails{margin-left:0;} .thumbnails>li{float:none;margin-left:0;} [class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{float:none;display:block;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .row-fluid [class*="offset"]:first-child{margin-left:0;} .input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto;} .controls-row [class*="span"]+[class*="span"]{margin-left:0;} .modal{position:fixed;top:20px;left:20px;right:20px;width:auto;margin:0;}.modal.fade{top:-100px;} .modal.fade.in{top:20px;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:20px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .media .pull-left,.media .pull-right{float:none;display:block;margin-bottom:10px;} .media-object{margin-right:0;margin-left:0;} .modal{top:10px;left:10px;right:10px;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12,textarea.span12,.uneditable-input.span12{width:710px;} input.span11,textarea.span11,.uneditable-input.span11{width:648px;} input.span10,textarea.span10,.uneditable-input.span10{width:586px;} input.span9,textarea.span9,.uneditable-input.span9{width:524px;} input.span8,textarea.span8,.uneditable-input.span8{width:462px;} input.span7,textarea.span7,.uneditable-input.span7{width:400px;} input.span6,textarea.span6,.uneditable-input.span6{width:338px;} input.span5,textarea.span5,.uneditable-input.span5{width:276px;} input.span4,textarea.span4,.uneditable-input.span4{width:214px;} input.span3,textarea.span3,.uneditable-input.span3{width:152px;} input.span2,textarea.span2,.uneditable-input.span2{width:90px;} input.span1,textarea.span1,.uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12,textarea.span12,.uneditable-input.span12{width:1156px;} input.span11,textarea.span11,.uneditable-input.span11{width:1056px;} input.span10,textarea.span10,.uneditable-input.span10{width:956px;} input.span9,textarea.span9,.uneditable-input.span9{width:856px;} input.span8,textarea.span8,.uneditable-input.span8{width:756px;} input.span7,textarea.span7,.uneditable-input.span7{width:656px;} input.span6,textarea.span6,.uneditable-input.span6{width:556px;} input.span5,textarea.span5,.uneditable-input.span5{width:456px;} input.span4,textarea.span4,.uneditable-input.span4{width:356px;} input.span3,textarea.span3,.uneditable-input.span3{width:256px;} input.span2,textarea.span2,.uneditable-input.span2{width:156px;} input.span1,textarea.span1,.uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top,.navbar-fixed-bottom{position:static;} .navbar-fixed-top{margin-bottom:20px;} .navbar-fixed-bottom{margin-top:20px;} .navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .nav-collapse{clear:both;} .nav-collapse .nav{float:none;margin:0 0 10px;} .nav-collapse .nav>li{float:none;} .nav-collapse .nav>li>a{margin-bottom:2px;} .nav-collapse .nav>.divider-vertical{display:none;} .nav-collapse .nav .nav-header{color:#777777;text-shadow:none;} .nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} .nav-collapse .dropdown-menu li+li a{margin-bottom:2px;} .nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2;} .navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999999;} .navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111111;} .nav-collapse.in .btn-group{margin-top:5px;padding:0;} .nav-collapse .dropdown-menu{position:static;top:auto;left:auto;float:none;display:none;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .nav-collapse .open>.dropdown-menu{display:block;} .nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none;} .nav-collapse .dropdown-menu .divider{display:none;} .nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none;} .nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);} .navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111111;border-bottom-color:#111111;} .navbar .nav-collapse .nav.pull-right{float:none;margin-left:0;} .nav-collapse,.nav-collapse.collapse{overflow:hidden;height:0;} .navbar .btn-navbar{display:block;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}} #memberentry-form input.error, form[name=opacmessaging] input.error {border-color: #c00; box-shadow: 0 1px 1px #c00 inset, 0 0 8px #c00; color: red; outline: 0 none;} #memberentry-form input.error:focus, form[name=opacmessaging] input.error:focus {border-color: #c00; box-shadow: 0 1px 1px #c00 inset, 0 0 8px #c00; color: red; outline: 0 none;} #memberentry-form label.error, form[name=opacmessaging] label.error {color: #c00; float: none; font-size: 90%;} --- a/members/memberentry.pl +++ a/members/memberentry.pl @@ -42,12 +42,13 @@ use C4::Branch; # GetBranches use C4::Form::MessagingPreferences; use Koha::Borrower::Debarments; use Koha::DateUtils; -use Email::Valid; +use Koha::Validation; use Module::Load; if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { load Koha::NorwegianPatronDB, qw( NLGetSyncDataFromBorrowernumber ); } +use re 'regexp_pattern'; use vars qw($debug); BEGIN { @@ -327,20 +328,14 @@ if ($op eq 'save' || $op eq 'insert'){ push @errors, "ERROR_password_mismatch" if ( $password ne $password2 ); push @errors, "ERROR_short_password" if( $password && $minpw && $password ne '****' && (length($password) < $minpw) ); - # Validate emails - my $emailprimary = $input->param('email'); - my $emailsecondary = $input->param('emailpro'); - my $emailalt = $input->param('B_email'); - - if ($emailprimary) { - push (@errors, "ERROR_bad_email") if (!Email::Valid->address($emailprimary)); - } - if ($emailsecondary) { - push (@errors, "ERROR_bad_email_secondary") if (!Email::Valid->address($emailsecondary)); - } - if ($emailalt) { - push (@errors, "ERROR_bad_email_alternative") if (!Email::Valid->address($emailalt)); - } + # validate emails + push (@errors, "ERROR_bad_email") if ($input->param('email') && !Koha::Validation::validate_email($input->param('email'))); + push (@errors, "ERROR_bad_email_secondary") if ($input->param('emailpro') && !Koha::Validation::validate_email($input->param('emailpro'))); + push (@errors, "ERROR_bad_email_alternative") if ($input->param('B_email') && !Koha::Validation::validate_email($input->param('B_email'))); + # validate phone numbers + push (@errors, "ERROR_bad_phone") if ($input->param('phone') && !Koha::Validation::validate_phonenumber($input->param('phone'))); + push (@errors, "ERROR_bad_phone_secondary") if ($input->param('phonepro') && !Koha::Validation::validate_phonenumber($input->param('phonepro'))); + push (@errors, "ERROR_bad_phone_alternative") if ($input->param('B_phone') && !Koha::Validation::validate_phonenumber($input->param('B_phone'))); if (C4::Context->preference('ExtendedPatronAttributes')) { $extended_patron_attributes = parse_extended_patron_attributes($input); @@ -372,7 +367,12 @@ if ( or $input->param('SMSnumber') ne $newdata{'mobile'} ) ) { - $newdata{smsalertnumber} = $input->param('SMSnumber'); + if (Koha::Validation::validate_phonenumber($input->param('SMSnumber'))){ + $newdata{smsalertnumber} = $input->param('SMSnumber'); + } else { + push (@errors, "ERROR_bad_smsnumber"); + } + } ### Error checks should happen before this line. @@ -681,6 +681,12 @@ if (C4::Context->preference('ExtendedPatronAttributes')) { patron_attributes_form($template, $borrowernumber); } +$template->param( + ValidateEmailAddress => C4::Context->preference('ValidateEmailAddress'), + ValidatePhoneNumber => C4::Context->preference('ValidatePhoneNumber') ne "OFF", + phone_regex => regexp_pattern Koha::Validation::get_phonenumber_regex() +); + if (C4::Context->preference('EnhancedMessagingPreferences')) { if ($op eq 'add') { C4::Form::MessagingPreferences::set_form_values({ categorycode => $categorycode }, $template); --- a/opac/opac-memberentry.pl +++ a/opac/opac-memberentry.pl @@ -28,7 +28,9 @@ use C4::Form::MessagingPreferences; use Koha::Borrower::Modifications; use C4::Branch qw(GetBranchesLoop); use C4::Scrubber; -use Email::Valid; +use Koha::Validation; + +use re 'regexp_pattern'; my $cgi = new CGI; my $dbh = C4::Context->dbh; @@ -68,6 +70,9 @@ $template->param( member_titles => GetTitles() || undef, branches => GetBranchesLoop(), OPACPatronDetails => C4::Context->preference('OPACPatronDetails'), + ValidateEmailAddress => C4::Context->preference('ValidateEmailAddress'), + ValidatePhoneNumber => (C4::Context->preference('ValidatePhoneNumber') ne "OFF"), + phone_regex => regexp_pattern Koha::Validation::get_phonenumber_regex() ); if ( $action eq 'create' ) { @@ -307,13 +312,25 @@ sub CheckForInvalidFields { my $borrower = shift; my @invalidFields; if ($borrower->{'email'}) { - push(@invalidFields, "email") if (!Email::Valid->address($borrower->{'email'})); + push(@invalidFields, "email") if (!Koha::Validation::validate_email($borrower->{'email'})); } if ($borrower->{'emailpro'}) { - push(@invalidFields, "emailpro") if (!Email::Valid->address($borrower->{'emailpro'})); + push(@invalidFields, "emailpro") if (!Koha::Validation::validate_email($borrower->{'emailpro'})); } if ($borrower->{'B_email'}) { - push(@invalidFields, "B_email") if (!Email::Valid->address($borrower->{'B_email'})); + push(@invalidFields, "B_email") if (!Koha::Validation::validate_email($borrower->{'B_email'})); + } + if ($borrower->{'mobile'}) { + push(@invalidFields, "mobile") if (!Koha::Validation::validate_phonenumber($borrower->{'mobile'})); + } + if ($borrower->{'phone'}) { + push(@invalidFields, "phone") if (!Koha::Validation::validate_phonenumber($borrower->{'phone'})); + } + if ($borrower->{'phonepro'}) { + push(@invalidFields, "phonepro") if (!Koha::Validation::validate_phonenumber($borrower->{'phonepro'})); + } + if ($borrower->{'B_phone'}) { + push(@invalidFields, "B_phone") if (!Koha::Validation::validate_phonenumber($borrower->{'B_phone'})); } return \@invalidFields; } --- a/opac/opac-messaging.pl +++ a/opac/opac-messaging.pl @@ -32,6 +32,9 @@ use C4::Members; use C4::Members::Messaging; use C4::Branch; use C4::Form::MessagingPreferences; +use Koha::Validation; + +use re 'regexp_pattern'; my $query = CGI->new(); @@ -48,10 +51,18 @@ my ( $template, $borrowernumber, $cookie ) = get_template_and_user( my $borrower = GetMemberDetails( $borrowernumber ); my $messaging_options = C4::Members::Messaging::GetMessagingOptions(); +$template->param( + ValidateEmailAddress => C4::Context->preference('ValidateEmailAddress'), + ValidatePhoneNumber => (C4::Context->preference('ValidatePhoneNumber') ne "OFF"), + phone_regex => regexp_pattern Koha::Validation::get_phonenumber_regex() +); if ( defined $query->param('modify') && $query->param('modify') eq 'yes' ) { + my $valid_smsnumber = Koha::Validation::validate_phonenumber($query->param('SMSnumber')); + $template->param( invalid_smsnumber => $valid_smsnumber ); + # If they've modified the SMS number, record it. - if ( ( defined $query->param('SMSnumber') ) && ( $query->param('SMSnumber') ne $borrower->{'mobile'} ) ) { + if ( ( defined $query->param('SMSnumber') ) && ( $query->param('SMSnumber') ne $borrower->{'mobile'} ) && $valid_smsnumber ) { ModMember( borrowernumber => $borrowernumber, smsalertnumber => $query->param('SMSnumber') ); $borrower = GetMemberDetails( $borrowernumber ); --- a/t/db_dependent/Validations.t +++ a/t/db_dependent/Validations.t @@ -0,0 +1,184 @@ +#!/usr/bin/env perl + +# Copyright 2015 Open Source Freedom Fighters +# +# 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 3 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, see . +$ENV{KOHA_PAGEOBJECT_DEBUG} = 1; +use Modern::Perl; + +use Test::More; +use Try::Tiny; #Even Selenium::Remote::Driver uses Try::Tiny :) + +use Koha::Auth::PermissionManager; + +use t::lib::Page::Mainpage; +use t::lib::Page::Opac::OpacMain; +use t::lib::Page::Opac::OpacMemberentry; +use t::lib::Page::Members::Memberentry; +use t::lib::Page::Members::Moremember; + +use t::lib::TestObjects::BorrowerFactory; +use t::lib::TestObjects::SystemPreferenceFactory; + +##Setting up the test context +my $testContext = {}; + +my $password = '1234'; +my $borrowerFactory = t::lib::TestObjects::BorrowerFactory->new(); +my $borrowers = $borrowerFactory->createTestGroup([ + {firstname => 'Testone', + surname => 'Testtwo', + cardnumber => '1A01', + branchcode => 'CPL', + userid => 'normal_user', + password => $password, + }, + {firstname => 'Testthree', + surname => 'Testfour', + cardnumber => 'superuberadmin', + branchcode => 'CPL', + userid => 'god', + password => $password, + }, + ], undef, $testContext); + +my $systempreferences = t::lib::TestObjects::SystemPreferenceFactory->createTestGroup([ + {preference => 'ValidateEmailAddress', + value => 1 + }, + {preference => 'ValidatePhoneNumber', + value => 'ipn', + }, + ], undef, $testContext); + +my $permissionManager = Koha::Auth::PermissionManager->new(); +$permissionManager->grantPermissions($borrowers->{'superuberadmin'}, {superlibrarian => 'superlibrarian'}); + +eval { + OpacValidations(); + StaffValidations(); +}; + +if ($@) { #Catch all leaking errors and gracefully terminate. + warn $@; + tearDown(); + exit 1; +} + +##All tests done, tear down test context +tearDown(); +done_testing; + +sub tearDown { + t::lib::TestObjects::ObjectFactory->tearDownTestContext($testContext); +} + + + + + + + + +###################################################### + ### STARTING TEST IMPLEMENTATIONS ### +###################################################### +sub setValidationsOff { + C4::Context->set_preference('ValidateEmailAddress', 0); + C4::Context->set_preference('ValidatePhoneNumber', 'OFF'); +} +sub setValidationsOn { + C4::Context->set_preference('ValidateEmailAddress', 1); + C4::Context->set_preference('ValidatePhoneNumber', 'ipn'); +} + +sub OpacValidations { + my $main = t::lib::Page::Opac::OpacMain->new({borrowernumber => $borrowers->{'1A01'}->borrowernumber}); + + ok (0, "Email validation is OFF!") if (C4::Context->preference('ValidateEmailAddress') == 0); + ok (0, "Phone validation is OFF") if C4::Context->preference('ValidatePhoneNumber') eq "OFF"; + $main + ->doPasswordLogin($borrowers->{'1A01'}->userid(), $password) + ->navigateYourPersonalDetails() + ->setEmail("valid\@email.com") # test valid email + ->submitForm(1) # expecting success + ->navigateYourPersonalDetails() + ->setEmail("invalidemail") # test invalid email + ->submitForm(0) # expecting error + ->navigateYourPersonalDetails() + ->setPhone("+3585012345667") # test valid phone number + ->submitForm(1) # expecting success + ->navigateYourPersonalDetails() + ->setPhone("1234phone56789") # test invalid phone number + ->submitForm(0); # expecting error + + print "--Setting validations off--\n"; + setValidationsOff(); # set validations off from system prefs + #then test validations again + ok (0, "Email validation is ON!") if (C4::Context->preference('ValidateEmailAddress') == 1); + ok (0, "Phone validation is ON!") if C4::Context->preference('ValidatePhoneNumber') ne "OFF"; + + $main + ->navigateYourPersonalDetails() + ->setEmail("invalidemail_validations_off") # test invalid email + ->submitForm(1) # expecting success + ->navigateYourPersonalDetails() + ->setPhone("1234phone56789_validations_off") # test invalid phone number + ->submitForm(1); # expecting success + + setValidationsOn(); +} + +sub StaffValidations { + my $memberentry = t::lib::Page::Members::Memberentry->new({borrowernumber => $borrowers->{'superuberadmin'}->borrowernumber, op => 'modify', destination => 'circ', categorycode => 'PT'}); + + ok (0, "Email validation is OFF!") if (C4::Context->preference('ValidateEmailAddress') == 0); + ok (0, "Phone validation is OFF") if C4::Context->preference('ValidatePhoneNumber') eq "OFF"; + + $memberentry + ->doPasswordLogin($borrowers->{'superuberadmin'}->userid(), $password) + ->setEmail("valid\@email.com") # test valid email + ->submitForm(1) # expecting success + ->navigateEditPatron() + ->setEmail("invalidemail") # test invalid email + ->submitForm(0) # expecting error + ->setEmail("") + ->setPhone("+3585012345667") # test valid phone number + ->submitForm(1) # expecting success + ->navigateEditPatron() + ->setPhone("1234phone56789") # test invalid phone number + ->submitForm(0); # expecting error + + print "--Setting validations off--\n"; + setValidationsOff(); # set validations off from system prefs + #then test validations again + + ok (0, "Email validation is ON!") if (C4::Context->preference('ValidateEmailAddress') == 1); + ok (0, "Phone validation is ON!") if C4::Context->preference('ValidatePhoneNumber') ne "OFF"; + + $memberentry + ->setPhone("") # refreshing + ->setEmail("") # the + ->submitForm(1) # page + ->navigateEditPatron() + ->setEmail("invalidemail_validations_off") # test invalid email + ->submitForm(1) # expecting success + ->navigateEditPatron() + ->setPhone("1234phone56789_validations_off") # test invalid phone number + ->submitForm(1); # expecting success + + setValidationsOn(); +} --