From 468df23801810985c5040ac850c0bd76b07a0cd0 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Tue, 7 Apr 2015 15:56:00 +1200 Subject: [PATCH] Nested comments proof of concept Update theme to new look and feel Add jquery-validate 1.3.1 (w/ nz dst patch) --- .gitignore | 1 + _config.php | 3 +- code/controllers/CommentingController.php | 108 +- code/extensions/CommentsExtension.php | 66 +- code/{dataobjects => model}/Comment.php | 202 ++- code/{ => model}/CommentList.php | 0 config.rb | 23 + css/comments.css | 173 +++ javascript/CommentsInterface.js | 198 ++- scss/comments.scss | 190 +++ templates/CommentReplies.ss | 23 + templates/CommentsInterface.ss | 4 +- templates/CommentsInterface_singlecomment.ss | 32 +- templates/Includes/ReplyPagination.ss | 1 + tests/CommentingControllerTest.php | 114 ++ tests/CommentingControllerTests.php | 59 - .../jquery-validate/additional-methods.js | 940 ++++++++++++ .../jquery-validate/additional-methods.min.js | 4 + thirdparty/jquery-validate/jquery.validate.js | 1365 +++++++++++++++++ .../jquery-validate/jquery.validate.min.js | 4 + .../localization/messages_ar.js | 33 + .../localization/messages_bg.js | 33 + .../localization/messages_bn_BD.js | 33 + .../localization/messages_ca.js | 33 + .../localization/messages_cs.js | 33 + .../localization/messages_da.js | 30 + .../localization/messages_de.js | 30 + .../localization/messages_el.js | 33 + .../localization/messages_es.js | 36 + .../localization/messages_es_AR.js | 37 + .../localization/messages_et.js | 31 + .../localization/messages_eu.js | 33 + .../localization/messages_fa.js | 36 + .../localization/messages_fi.js | 31 + .../localization/messages_fr.js | 59 + .../localization/messages_gl.js | 38 + .../localization/messages_he.js | 33 + .../localization/messages_hr.js | 33 + .../localization/messages_hu.js | 32 + .../localization/messages_hy_AM.js | 33 + .../localization/messages_id.js | 32 + .../localization/messages_is.js | 31 + .../localization/messages_it.js | 36 + .../localization/messages_ja.js | 33 + .../localization/messages_ka.js | 33 + .../localization/messages_kk.js | 33 + .../localization/messages_ko.js | 33 + .../localization/messages_lt.js | 33 + .../localization/messages_lv.js | 33 + .../localization/messages_my.js | 33 + .../localization/messages_nl.js | 43 + .../localization/messages_no.js | 33 + .../localization/messages_pl.js | 33 + .../localization/messages_pt_BR.js | 38 + .../localization/messages_pt_PT.js | 37 + .../localization/messages_ro.js | 33 + .../localization/messages_ru.js | 33 + .../localization/messages_si.js | 33 + .../localization/messages_sk.js | 30 + .../localization/messages_sl.js | 33 + .../localization/messages_sr.js | 33 + .../localization/messages_sr_lat.js | 33 + .../localization/messages_sv.js | 31 + .../localization/messages_th.js | 33 + .../localization/messages_tj.js | 33 + .../localization/messages_tr.js | 33 + .../localization/messages_uk.js | 33 + .../localization/messages_vi.js | 33 + .../localization/messages_zh.js | 33 + .../localization/messages_zh_TW.js | 34 + .../localization/methods_de.js | 22 + .../localization/methods_es_CL.js | 22 + .../localization/methods_fi.js | 22 + .../localization/methods_nl.js | 19 + .../localization/methods_pt.js | 19 + 75 files changed, 5102 insertions(+), 207 deletions(-) create mode 100644 .gitignore rename code/{dataobjects => model}/Comment.php (75%) rename code/{ => model}/CommentList.php (100%) create mode 100644 config.rb create mode 100644 css/comments.css create mode 100644 scss/comments.scss create mode 100644 templates/CommentReplies.ss create mode 100644 templates/Includes/ReplyPagination.ss create mode 100644 tests/CommentingControllerTest.php delete mode 100644 tests/CommentingControllerTests.php create mode 100644 thirdparty/jquery-validate/additional-methods.js create mode 100644 thirdparty/jquery-validate/additional-methods.min.js create mode 100644 thirdparty/jquery-validate/jquery.validate.js create mode 100644 thirdparty/jquery-validate/jquery.validate.min.js create mode 100644 thirdparty/jquery-validate/localization/messages_ar.js create mode 100644 thirdparty/jquery-validate/localization/messages_bg.js create mode 100644 thirdparty/jquery-validate/localization/messages_bn_BD.js create mode 100644 thirdparty/jquery-validate/localization/messages_ca.js create mode 100644 thirdparty/jquery-validate/localization/messages_cs.js create mode 100644 thirdparty/jquery-validate/localization/messages_da.js create mode 100644 thirdparty/jquery-validate/localization/messages_de.js create mode 100644 thirdparty/jquery-validate/localization/messages_el.js create mode 100644 thirdparty/jquery-validate/localization/messages_es.js create mode 100644 thirdparty/jquery-validate/localization/messages_es_AR.js create mode 100644 thirdparty/jquery-validate/localization/messages_et.js create mode 100644 thirdparty/jquery-validate/localization/messages_eu.js create mode 100644 thirdparty/jquery-validate/localization/messages_fa.js create mode 100644 thirdparty/jquery-validate/localization/messages_fi.js create mode 100644 thirdparty/jquery-validate/localization/messages_fr.js create mode 100644 thirdparty/jquery-validate/localization/messages_gl.js create mode 100644 thirdparty/jquery-validate/localization/messages_he.js create mode 100644 thirdparty/jquery-validate/localization/messages_hr.js create mode 100644 thirdparty/jquery-validate/localization/messages_hu.js create mode 100644 thirdparty/jquery-validate/localization/messages_hy_AM.js create mode 100644 thirdparty/jquery-validate/localization/messages_id.js create mode 100644 thirdparty/jquery-validate/localization/messages_is.js create mode 100644 thirdparty/jquery-validate/localization/messages_it.js create mode 100644 thirdparty/jquery-validate/localization/messages_ja.js create mode 100644 thirdparty/jquery-validate/localization/messages_ka.js create mode 100644 thirdparty/jquery-validate/localization/messages_kk.js create mode 100644 thirdparty/jquery-validate/localization/messages_ko.js create mode 100644 thirdparty/jquery-validate/localization/messages_lt.js create mode 100644 thirdparty/jquery-validate/localization/messages_lv.js create mode 100644 thirdparty/jquery-validate/localization/messages_my.js create mode 100644 thirdparty/jquery-validate/localization/messages_nl.js create mode 100644 thirdparty/jquery-validate/localization/messages_no.js create mode 100644 thirdparty/jquery-validate/localization/messages_pl.js create mode 100644 thirdparty/jquery-validate/localization/messages_pt_BR.js create mode 100644 thirdparty/jquery-validate/localization/messages_pt_PT.js create mode 100644 thirdparty/jquery-validate/localization/messages_ro.js create mode 100644 thirdparty/jquery-validate/localization/messages_ru.js create mode 100644 thirdparty/jquery-validate/localization/messages_si.js create mode 100644 thirdparty/jquery-validate/localization/messages_sk.js create mode 100644 thirdparty/jquery-validate/localization/messages_sl.js create mode 100644 thirdparty/jquery-validate/localization/messages_sr.js create mode 100644 thirdparty/jquery-validate/localization/messages_sr_lat.js create mode 100644 thirdparty/jquery-validate/localization/messages_sv.js create mode 100644 thirdparty/jquery-validate/localization/messages_th.js create mode 100644 thirdparty/jquery-validate/localization/messages_tj.js create mode 100644 thirdparty/jquery-validate/localization/messages_tr.js create mode 100644 thirdparty/jquery-validate/localization/messages_uk.js create mode 100644 thirdparty/jquery-validate/localization/messages_vi.js create mode 100644 thirdparty/jquery-validate/localization/messages_zh.js create mode 100644 thirdparty/jquery-validate/localization/messages_zh_TW.js create mode 100644 thirdparty/jquery-validate/localization/methods_de.js create mode 100644 thirdparty/jquery-validate/localization/methods_es_CL.js create mode 100644 thirdparty/jquery-validate/localization/methods_fi.js create mode 100644 thirdparty/jquery-validate/localization/methods_nl.js create mode 100644 thirdparty/jquery-validate/localization/methods_pt.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5df1b9b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.sass-cache diff --git a/_config.php b/_config.php index 38c672a..3fbfa6d 100644 --- a/_config.php +++ b/_config.php @@ -2,4 +2,5 @@ Deprecation::notification_version('2.0', 'comments'); -define('COMMENTS_DIR', ltrim(Director::makeRelative(realpath(__DIR__)), DIRECTORY_SEPARATOR)); \ No newline at end of file +define('COMMENTS_DIR', basename(__DIR__)); +define('COMMENTS_THIRDPARTY', COMMENTS_DIR . DIRECTORY_SEPARATOR . 'thirdparty'); diff --git a/code/controllers/CommentingController.php b/code/controllers/CommentingController.php index 844bb19..789f6ba 100644 --- a/code/controllers/CommentingController.php +++ b/code/controllers/CommentingController.php @@ -13,10 +13,27 @@ class CommentingController extends Controller { 'approve', 'rss', 'CommentsForm', + 'reply', 'doPostComment', 'doPreviewComment' ); + private static $url_handlers = array( + 'reply/$ParentCommentID//$ID/$OtherID' => 'reply', + ); + + /** + * Fields required for this form + * + * @var array + * @config + */ + private static $required_fields = array( + 'Name', + 'Email', + 'Comment' + ); + /** * Base class this commenting form is for * @@ -289,6 +306,47 @@ class CommentingController extends Controller { return false; } + /** + * Create a reply form for a specified comment + * + * @param Comment $comment + */ + public function ReplyForm($comment) { + // Enables multiple forms with different names to use the same handler + $form = $this->CommentsForm(); + $form->setName('ReplyForm_'.$comment->ID); + $form->addExtraClass('reply-form'); + + // Load parent into reply form + $form->loadDataFrom(array( + 'ParentCommentID' => $comment->ID + )); + + // Customise action + $form->setFormAction($this->Link('reply', $comment->ID)); + + $this->extend('updateReplyForm', $form); + return $form; + } + + + /** + * Request handler for reply form. + * This method will disambiguate multiple reply forms in the same method + * + * @param SS_HTTPRequest $request + */ + public function reply(SS_HTTPRequest $request) { + // Extract parent comment from reply and build this way + if($parentID = $request->param('ParentCommentID')) { + $comment = DataObject::get_by_id('Comment', $parentID, true); + if($comment) { + return $this->ReplyForm($comment); + } + } + return $this->httpError(404); + } + /** * Post a comment form * @@ -297,26 +355,42 @@ class CommentingController extends Controller { public function CommentsForm() { $usePreview = $this->getOption('use_preview'); + $nameRequired = _t('CommentInterface.YOURNAME_MESSAGE_REQUIRED', 'Please enter your name'); + $emailRequired = _t('CommentInterface.EMAILADDRESS_MESSAGE_REQUIRED', 'Please enter your email address'); + $emailInvalid = _t('CommentInterface.EMAILADDRESS_MESSAGE_EMAIL', 'Please enter a valid email address'); + $urlInvalid = _t('CommentInterface.COMMENT_MESSAGE_URL', 'Please enter a valid URL'); + $commentRequired = _t('CommentInterface.COMMENT_MESSAGE_REQUIRED', 'Please enter your comment'); + $fields = new FieldList( $dataFields = new CompositeField( - TextField::create("Name", _t('CommentInterface.YOURNAME', 'Your name')) - ->setCustomValidationMessage(_t('CommentInterface.YOURNAME_MESSAGE_REQUIRED', 'Please enter your name')) - ->setAttribute('data-message-required', _t('CommentInterface.YOURNAME_MESSAGE_REQUIRED', 'Please enter your name')), + // Name + TextField::create("Name", _t('CommentInterface.YOURNAME', 'Your name')) + ->setCustomValidationMessage($nameRequired) + ->setAttribute('data-msg-required', $nameRequired), - EmailField::create("Email", _t('CommentingController.EMAILADDRESS', "Your email address (will not be published)")) - ->setCustomValidationMessage(_t('CommentInterface.EMAILADDRESS_MESSAGE_REQUIRED', 'Please enter your email address')) - ->setAttribute('data-message-required', _t('CommentInterface.EMAILADDRESS_MESSAGE_REQUIRED', 'Please enter your email address')) - ->setAttribute('data-message-email', _t('CommentInterface.EMAILADDRESS_MESSAGE_EMAIL', 'Please enter a valid email address')), + // Email + EmailField::create( + "Email", + _t('CommentingController.EMAILADDRESS', "Your email address (will not be published)") + ) + ->setCustomValidationMessage($emailRequired) + ->setAttribute('data-msg-required', $emailRequired) + ->setAttribute('data-msg-email', $emailInvalid) + ->setAttribute('data-rule-email', true), - TextField::create("URL", _t('CommentingController.WEBSITEURL', "Your website URL")) - ->setAttribute('data-message-url', _t('CommentInterface.COMMENT_MESSAGE_URL', 'Please enter a valid URL')), + // Url + TextField::create("URL", _t('CommentingController.WEBSITEURL', "Your website URL")) + ->setAttribute('data-msg-url', $urlInvalid) + ->setAttribute('data-rule-url', true), - TextareaField::create("Comment", _t('CommentingController.COMMENTS', "Comments")) - ->setCustomValidationMessage(_t('CommentInterface.COMMENT_MESSAGE_REQUIRED', 'Please enter your comment')) - ->setAttribute('data-message-required', _t('CommentInterface.COMMENT_MESSAGE_REQUIRED', 'Please enter your comment')) + // Comment + TextareaField::create("Comment", _t('CommentingController.COMMENTS', "Comments")) + ->setCustomValidationMessage($commentRequired) + ->setAttribute('data-msg-required', $commentRequired) ), HiddenField::create("ParentID"), HiddenField::create("ReturnURL"), + HiddenField::create("ParentCommentID"), HiddenField::create("BaseClass") ); @@ -345,11 +419,7 @@ class CommentingController extends Controller { } // required fields for server side - $required = new RequiredFields(array( - 'Name', - 'Email', - 'Comment' - )); + $required = new RequiredFields($this->config()->required_fields); // create the comment form $form = new Form($this, 'CommentsForm', $fields, $actions, $required); @@ -397,8 +467,8 @@ class CommentingController extends Controller { } } - if($member) { - $form->loadDataFrom($member); + if(!empty($member)) { + $form->loadDataFrom($member); } // hook to allow further extensions to alter the comments form diff --git a/code/extensions/CommentsExtension.php b/code/extensions/CommentsExtension.php index 21e240a..d39ed5c 100644 --- a/code/extensions/CommentsExtension.php +++ b/code/extensions/CommentsExtension.php @@ -19,6 +19,8 @@ class CommentsExtension extends DataExtension { * gravatar_default: Theme for 'not found' gravatar {@see http://gravatar.com/site/implement/images} * gravatar_rating: Gravatar rating (same as the standard default) * show_comments_when_disabled: Show older comments when commenting has been disabled. + * order_comments_by: Default sort order. + * order_replies_by: Sort order for replies. * comments_holder_id: ID for the comments holder * comment_permalink_prefix: ID prefix for each comment * require_moderation: Require moderation for all comments @@ -27,6 +29,8 @@ class CommentsExtension extends DataExtension { * frontend_spam: Display spam comments in the frontend, if the user can moderate them. * html_allowed: Allow for sanitized HTML in comments * use_preview: Preview formatted comment (when allowing HTML) + * nested_comments: Enable nested comments + * nested_depth: Max depth of nested comments in levels (where root is 1 depth) 0 means no limit. * * @var array * @@ -45,6 +49,7 @@ class CommentsExtension extends DataExtension { 'gravatar_rating' => 'g', 'show_comments_when_disabled' => false, 'order_comments_by' => '"Created" DESC', + 'order_replies_by' => false, 'comments_per_page' => 10, 'comments_holder_id' => 'comments-holder', 'comment_permalink_prefix' => 'comment-', @@ -56,6 +61,8 @@ class CommentsExtension extends DataExtension { 'html_allowed' => false, 'html_allowed_elements' => array('a', 'img', 'i', 'b'), 'use_preview' => false, + 'nested_comments' => false, + 'nested_depth' => 2, ); /** @@ -141,24 +148,6 @@ class CommentsExtension extends DataExtension { } } - /** - * Returns the RelationList of all comments against this object. Can be used as a data source - * for a gridfield with write access. - * - * @return CommentList - */ - public function AllComments() { - $comments = CommentList::create($this->ownerBaseClass)->forForeignID($this->owner->ID); - $this->owner->extend('updateAllComments', $comments); - return $comments; - } - - public function getComments() { - // TODO: find out why this is being triggered when combined with blog - // Deprecation::notice('2.0', 'Use PagedComments to get paged comments'); - return $this->PagedComments(); - } - /** * Get comment moderation rules for this parent * @@ -194,16 +183,27 @@ class CommentsExtension extends DataExtension { } /** - * Returns the root level comments, with spam and unmoderated items excluded, for use in the frontend + * Returns the RelationList of all comments against this object. Can be used as a data source + * for a gridfield with write access. * * @return CommentList */ - public function Comments() { - // Get all non-spam comments + public function AllComments() { $order = $this->owner->getCommentsOption('order_comments_by'); - $list = $this - ->AllComments() + $comments = CommentList::create($this->ownerBaseClass) + ->forForeignID($this->owner->ID) ->sort($order); + $this->owner->extend('updateAllComments', $comments); + return $comments; + } + + /** + * Returns all comments against this object, with with spam and unmoderated items excluded, for use in the frontend + * + * @return CommentList + */ + public function AllVisibleComments() { + $list = $this->AllComments(); // Filter spam comments for non-administrators if configured $showSpam = $this->owner->getCommentsOption('frontend_spam') && $this->owner->canModerateComments(); @@ -218,6 +218,23 @@ class CommentsExtension extends DataExtension { $list = $list->filter('Moderated', 1); } + $this->owner->extend('updateAllVisibleComments', $list); + return $list; + } + + /** + * Returns the root level comments, with spam and unmoderated items excluded, for use in the frontend + * + * @return CommentList + */ + public function Comments() { + $list = $this->AllVisibleComments(); + + // If nesting comments, only show root level + if($this->owner->getCommentsOption('nested_comments')) { + $list = $list->filter('ParentCommentID', 0); + } + $this->owner->extend('updateComments', $list); return $list; } @@ -384,8 +401,9 @@ class CommentsExtension extends DataExtension { $enabled = $this->getCommentsEnabled(); if($enabled && $this->owner->getCommentsOption('include_js')) { Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); + Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-validate/lib/jquery.form.js'); - Requirements::javascript(THIRDPARTY_DIR . '/jquery-validate/jquery.validate.pack.js'); + Requirements::javascript(COMMENTS_THIRDPARTY . '/jquery-validate/jquery.validate.min.js'); Requirements::javascript('comments/javascript/CommentsInterface.js'); } diff --git a/code/dataobjects/Comment.php b/code/model/Comment.php similarity index 75% rename from code/dataobjects/Comment.php rename to code/model/Comment.php index 2ec2084..ce89529 100755 --- a/code/dataobjects/Comment.php +++ b/code/model/Comment.php @@ -13,13 +13,15 @@ * @property integer $ParentID ID of the parent page / dataobject * @property boolean $AllowHtml If true, treat $Comment as HTML instead of plain text * @property string $SecretToken Secret admin token required to provide moderation links between sessions + * @property integer $Depth Depth of this comment in the nested chain * * @method HasManyList ChildComments() List of child comments * @method Member Author() Member object who created this comment - * + * @method Comment ParentComment() Parent comment this is a reply to * @package comments */ class Comment extends DataObject { + /** * @var array */ @@ -34,10 +36,16 @@ class Comment extends DataObject { 'ParentID' => 'Int', 'AllowHtml' => 'Boolean', 'SecretToken' => 'Varchar(255)', + 'Depth' => 'Int', ); private static $has_one = array( - 'Author' => 'Member', + "Author" => "Member", + "ParentComment" => "Comment", + ); + + private static $has_many = array( + "ChildComments" => "Comment" ); private static $default_sort = '"Created" DESC'; @@ -71,12 +79,16 @@ class Comment extends DataObject { private static $summary_fields = array( 'Name' => 'Submitted By', 'Email' => 'Email', - 'Comment' => 'Comment', + 'Comment.LimitWordCount' => 'Comment', 'Created' => 'Date Posted', 'ParentTitle' => 'Post', 'IsSpam' => 'Is Spam', ); + private static $field_labels = array( + 'Author' => 'Author Member', + ); + public function onBeforeWrite() { parent::onBeforeWrite(); @@ -84,6 +96,18 @@ class Comment extends DataObject { if($this->AllowHtml) { $this->Comment = $this->purifyHtml($this->Comment); } + + // Check comment depth + $this->updateDepth(); + } + + public function onBeforeDelete() { + parent::onBeforeDelete(); + + // Delete all children + foreach($this->ChildComments() as $comment) { + $comment->delete(); + } } /** @@ -224,7 +248,7 @@ class Comment extends DataObject { public function castingHelper($field) { // Safely escape the comment if($field === 'EscapedComment') { - return $this->AllowHtml ? 'HTMLText' : 'Varchar'; + return $this->AllowHtml ? 'HTMLText' : 'Text'; } return parent::castingHelper($field); } @@ -474,6 +498,7 @@ class Comment extends DataObject { $this->write(); $this->extend('afterMarkUnapproved'); } + /** * @return string */ @@ -506,14 +531,46 @@ class Comment extends DataObject { * Modify the default fields shown to the user */ public function getCMSFields() { - $fields = parent::getCMSFields(); + $commentField = $this->AllowHtml ? 'HtmlEditorField' : 'TextareaField'; + $fields = new FieldList( + $this + ->obj('Created') + ->scaffoldFormField($this->fieldLabel('Created')) + ->performReadonlyTransformation(), + TextField::create('Name', $this->fieldLabel('AuthorName')), + $commentField::create('Comment', $this->fieldLabel('Comment')), + EmailField::create('Email', $this->fieldLabel('Email')), + TextField::create('URL', $this->fieldLabel('URL')), + FieldGroup::create(array( + CheckboxField::create('Moderated', $this->fieldLabel('Moderated')), + CheckboxField::create('IsSpam', $this->fieldLabel('IsSpam')), + )) + ->setTitle('Options') + ->setDescription(_t( + 'Comment.OPTION_DESCRIPTION', + 'Unmoderated and spam comments will not be displayed until approved' + )) + ); - $hidden = array('ParentID', 'AuthorID', 'BaseClass', 'AllowHtml', 'SecretToken'); - - foreach($hidden as $private) { - $fields->removeByName($private); + // Show member name if given + if(($author = $this->Author()) && $author->exists()) { + $fields->insertAfter( + TextField::create('AuthorMember', $this->fieldLabel('Author'), $author->Title) + ->performReadonlyTransformation(), + 'Name' + ); } + // Show parent comment details + if(($parent = $this->ParentComment()) && $parent->exists()) { + $fields->insertAfter( + TextField::create('ParentCommentDescription', $this->fieldLabel('ParentComment'), $parent->Title) + ->performReadonlyTransformation(), + 'Created' + ); + } + + $this->extend('updateCMSFields', $fields); return $fields; } @@ -558,8 +615,135 @@ class Comment extends DataObject { return $gravatar; } + + /** + * Determine if replies are enabled for this instance + * + * @return boolean + */ + public function getRepliesEnabled() { + // Check reply option + if(!$this->getOption('nested_comments')) { + return false; + } + + // Check if depth is limited + $maxLevel = $this->getOption('nested_depth'); + return !$maxLevel || $this->Depth < $maxLevel; + } + + /** + * Returns the list of all replies + * + * @return SS_List + */ + public function AllReplies() { + // No replies if disabled + if(!$this->getRepliesEnabled()) { + return new ArrayList(); + } + + // Get all non-spam comments + $order = $this->getOption('order_replies_by') + ?: $this->getOption('order_comments_by'); + $list = $this + ->ChildComments() + ->sort($order); + + $this->extend('updateAllReplies', $list); + return $list; + } + + /** + * Returns the list of replies, with spam and unmoderated items excluded, for use in the frontend + * + * @return SS_List + */ + public function Replies() { + // No replies if disabled + if(!$this->getRepliesEnabled()) { + return new ArrayList(); + } + $list = $this->AllReplies(); + + // Filter spam comments for non-administrators if configured + $parent = $this->getParent(); + $showSpam = $this->getOption('frontend_spam') && $parent && $parent->canModerateComments(); + if(!$showSpam) { + $list = $list->filter('IsSpam', 0); + } + + // Filter un-moderated comments for non-administrators if moderation is enabled + $showUnmoderated = $parent && ( + ($parent->ModerationRequired === 'None') + || ($this->getOption('frontend_moderation') && $parent->canModerateComments()) + ); + if (!$showUnmoderated) { + $list = $list->filter('Moderated', 1); + } + + $this->extend('updateReplies', $list); + return $list; + } + + /** + * Returns the list of replies paged, with spam and unmoderated items excluded, for use in the frontend + * + * @return PaginatedList + */ + public function PagedReplies() { + $list = $this->Replies(); + + // Add pagination + $list = new PaginatedList($list, Controller::curr()->getRequest()); + $list->setPaginationGetVar('repliesstart'.$this->ID); + $list->setPageLength($this->getOption('comments_per_page')); + + $this->extend('updatePagedReplies', $list); + return $list; + } + + /** + * Generate a reply form for this comment + * + * @return Form + */ + public function ReplyForm() { + // Ensure replies are enabled + if(!$this->getRepliesEnabled()) { + return null; + } + + // Check parent is available + $parent = $this->getParent(); + if(!$parent || !$parent->exists()) { + return null; + } + + // Build reply controller + $controller = CommentingController::create(); + $controller->setOwnerRecord($parent); + $controller->setBaseClass($parent->ClassName); + $controller->setOwnerController(Controller::curr()); + + return $controller->ReplyForm($this); + } + + /** + * Refresh of this comment in the hierarchy + */ + public function updateDepth() { + $parent = $this->ParentComment(); + if($parent && $parent->exists()) { + $parent->updateDepth(); + $this->Depth = $parent->Depth + 1; + } else { + $this->Depth = 1; + } + } } + /** * Provides the ability to generate cryptographically secure tokens for comment moderation */ diff --git a/code/CommentList.php b/code/model/CommentList.php similarity index 100% rename from code/CommentList.php rename to code/model/CommentList.php diff --git a/config.rb b/config.rb new file mode 100644 index 0000000..a5dcfd2 --- /dev/null +++ b/config.rb @@ -0,0 +1,23 @@ +# Require any additional compass plugins here. + +# Set this to the root of your project when deployed: +http_path = "/" +css_dir = "css" +sass_dir = "scss" +javascripts_dir = "javascript" + +# You can select your preferred output style here (can be overridden via the command line): +# output_style = :expanded or :nested or :compact or :compressed + +# To enable relative paths to assets via compass helper functions. Uncomment: +relative_assets = true + +# To disable debugging comments that display the original location of your selectors. Uncomment: +line_comments = true + + +# If you prefer the indented syntax, you might want to regenerate this +# project again passing --syntax sass, or you can uncomment this: +# preferred_syntax = :sass +# and then run: +# sass-convert -R --from scss --to sass scss scss && rm -rf sass && mv scss sass diff --git a/css/comments.css b/css/comments.css new file mode 100644 index 0000000..fe51670 --- /dev/null +++ b/css/comments.css @@ -0,0 +1,173 @@ +/* line 12, ../scss/comments.scss */ +#comments-holder { + clear: both; +} +/* line 15, ../scss/comments.scss */ +#comments-holder h2 { + margin: 30px 0 10px; + padding-bottom: 5px; + border-bottom: 1px solid #eee; +} +/* line 21, ../scss/comments.scss */ +#comments-holder h3 { + margin-top: 0; +} +/* line 25, ../scss/comments.scss */ +#comments-holder .field { + clear: left; +} +/* line 29, ../scss/comments.scss */ +#comments-holder .num, +#comments-holder .author { + font-size: 1.3em; +} +/* line 34, ../scss/comments.scss */ +#comments-holder .num { + color: #999; + margin-right: 5px; +} +/* line 39, ../scss/comments.scss */ +#comments-holder .num-total { + line-height: 40px; + margin-bottom: 0; +} +/* line 44, ../scss/comments.scss */ +#comments-holder .comments-list { + margin: 0; +} +/* line 49, ../scss/comments.scss */ +#comments-holder .comment { + clear: both; + list-style-type: none; + overflow: auto; + padding: 20px 0 10px; + position: relative; +} +/* line 57, ../scss/comments.scss */ +#comments-holder .comment.author-comment:after { + content: 'Author'; + float: right; + position: absolute; + top: 1.5em; + right: 0; + font-size: 1em; + font-weight: bold; + color: #00acee; +} +/* line 68, ../scss/comments.scss */ +#comments-holder .comment.author-comment .comment-text { + border: 1px solid #00acee; +} +/* line 75, ../scss/comments.scss */ +#comments-holder .comment.spam .comment-text { + border: 1px dashed #f48b33; +} +/* line 80, ../scss/comments.scss */ +#comments-holder .comment .comment-text { + background-color: #fff; + border: 1px solid #ddd; + box-shadow: none; + margin: 0; + padding: 0 15% 0 20px; + white-space: pre; + white-space: pre-wrap; + white-space: pre-line; + word-wrap: break-word; +} +/* line 91, ../scss/comments.scss */ +#comments-holder .comment .comment-text p:last-child { + margin-bottom: 0; +} +/* line 96, ../scss/comments.scss */ +#comments-holder .comment .date { + font-size: 16px; +} +/* line 99, ../scss/comments.scss */ +#comments-holder .comment .date:before { + content: '\0000a0\0000a0\0000a0\0000a0'; +} +/* line 104, ../scss/comments.scss */ +#comments-holder .comment.unmoderated { + border: 2px dashed gray; + margin: 2em 0 4em; + padding: 2em; +} +/* line 109, ../scss/comments.scss */ +#comments-holder .comment.unmoderated .comment-moderation { + margin-bottom: 0; +} +/* line 114, ../scss/comments.scss */ +#comments-holder .comment .info { + margin-bottom: 10px; +} +/* line 118, ../scss/comments.scss */ +#comments-holder .comment.spam .comment { + border: 1px dashed #f48b33; + color: #f48b33; + border-radius: 4px; + padding: 2.5em 1em 1em; +} +/* line 125, ../scss/comments.scss */ +#comments-holder .comment .comment-replies-container { + margin-left: 20px; + padding-left: 10px; + border-left: 1px dashed #999; +} +/* line 130, ../scss/comments.scss */ +#comments-holder .comment .comment-replies-container .comment-reply-form-holder { + padding: 0 10px; +} +/* line 133, ../scss/comments.scss */ +#comments-holder .comment .comment-replies-container .comment-replies-holder { + padding: 0 0 0 10px; +} +/* line 140, ../scss/comments.scss */ +#comments-holder .comment-moderation { + float: right; + margin: 1em 0 2em; +} +/* line 144, ../scss/comments.scss */ +#comments-holder .comment-moderation .heading { + margin-bottom: 0; +} +/* line 149, ../scss/comments.scss */ +#comments-holder .action-links { + margin: 20px 0 10px; +} +/* line 152, ../scss/comments.scss */ +#comments-holder .action-links li { + display: inline; + list-style-type: none; + margin-left: 20px; + overflow: auto; +} +/* line 158, ../scss/comments.scss */ +#comments-holder .action-links li:first-child { + margin-left: 0; +} +/* line 162, ../scss/comments.scss */ +#comments-holder .action-links li.comment-reply-action { + float: right; +} +/* line 168, ../scss/comments.scss */ +#comments-holder .comment-count { + margin: 15px 0; +} +/* line 173, ../scss/comments.scss */ +#comments-holder .commenting-area { + margin-top: 50px; +} +/* line 176, ../scss/comments.scss */ +#comments-holder .commenting-area label.left { + font-weight: normal; +} +/* line 181, ../scss/comments.scss */ +#comments-holder .commenting-rss-feed { + margin-top: 4em; + text-align: right; +} +/* line 186, ../scss/comments.scss */ +#comments-holder .no-comments-yet { + display: inline-block; + margin-top: 10px; +} diff --git a/javascript/CommentsInterface.js b/javascript/CommentsInterface.js index e29eb0a..d934db3 100755 --- a/javascript/CommentsInterface.js +++ b/javascript/CommentsInterface.js @@ -2,115 +2,142 @@ * @package comments */ (function($) { - $(document).ready(function () { - - var container = $('.comments-holder-container'), - commentsHolder = $('.comments-holder'), - commentsList = $('.comments-list', commentsHolder), - pagination = $('.comments-pagination'), - noCommentsYet = $('.no-comments-yet', commentsHolder), - form = $('form', container), - previewEl = form.find('#PreviewComment'); + $.entwine( "ss.comments", function($) { /** - * Init + * Enable form validation */ - previewEl.hide(); - $(':submit[name=action_doPreviewComment]').show(); + $('.comments-holder-container form').entwine({ + onmatch: function() { + + // @todo Reinstate preview-comment functionality - /** - * Validate - */ - form.validate({ - invalidHandler : function(form, validator){ - $('html, body').animate({ - scrollTop: $(validator.errorList[0].element).offset().top - 30 - }, 200); + /** + * Validate + */ + $(this).validate({ + + /** + * Ignore hidden elements in this form + */ + ignore: ':hidden', + + /** + * Use default 'required' for error labels + */ + errorClass: "required", + + /** + * Use span instead of labels + */ + errorElement: "span", + + /** + * On error, scroll to the invalid element + */ + invalidHandler : function(form, validator){ + $('html, body').animate({ + scrollTop: $(validator.errorList[0].element).offset().top - 30 + }, 200); + }, + + /** + * Ensure any new error message has the correct class and placement + */ + errorPlacement: function(error, element) { + error + .addClass('message') + .insertAfter(element); + } + }); + this._super(); }, - showErrors: function(errorMap, errorList) { - this.defaultShowErrors(); - // hack to add the extra classes we need to the validation message elements - form.find('span.error').addClass('message required'); - }, - - errorElement: "span", - errorClass: "error", - ignore: '.hidden', - rules: { - Name : { - required : true - }, - Email : { - required : true, - email : true - }, - Comment: { - required : true - }, - URL: { - url : true + onunmatch: function() { + this._super(); + } + }); + + /** + * Comment reply form + */ + $( ".comment-replies-container .comment-reply-form-holder" ).entwine({ + onmatch: function() { + // If and only if this is not the currently selected form, hide it on page load + var selectedHash = window.document.location.hash.substr(1), + form = $(this).children('.reply-form'); + if( !selectedHash || selectedHash !== form.prop( 'id' ) ) { + this.hide(); } + this._super(); }, - messages: { - Name : { - required : form.find('[name="Name"]').data('message-required') - }, - Email : { - required : form.find('[name="Email"]').data('message-required'), - email : form.find('[name="Email"]').data('message-email') - }, - Comment: { - required : form.find('[name="Comment"]').data('message-required') - }, - URL: { - url : form.find('[name="Comment"]').data('message-url') + onunmatch: function() { + this._super(); + } + }); + + /** + * Toggle on/off reply form + */ + $( ".comment-reply-link" ).entwine({ + onclick: function( e ) { + var allForms = $( ".comment-reply-form-holder" ), + formID = $( this ).prop('href').replace(/^[^#]*#/, '#'), + form = $(formID).closest('.comment-reply-form-holder'); + + // Prevent focus + e.preventDefault(); + if(form.is(':visible')) { + allForms.slideUp(); + } else { + allForms.not(form).slideUp(); + form.slideDown(); } } }); - - form.submit(function (e) { - // trigger validation - if(!form.validate().valid()) return false; - }); + /** * Preview comment by fetching it from the server via ajax. */ + /* @todo Migrate to work with nested comments $(':submit[name=action_doPreviewComment]', form).click(function(e) { - e.preventDefault(); + e.preventDefault(); - if(!form.validate().valid()) { - return false; - } + if(!form.validate().valid()) { + return false; + } - previewEl.show().addClass('loading').find('.middleColumn').html(' '); + previewEl.show().addClass('loading').find('.middleColumn').html(' '); - form.ajaxSubmit({ - success: function(response) { - var responseEl = $(response); - if(responseEl.is('form')) { - // Validation failed, renders form instead of single comment - form.find(".data-fields").replaceWith(responseEl.find(".data-fields")); - } else { - // Default behaviour - previewEl.removeClass('loading').find('.middleColumn').html(responseEl); - } - }, - data: {'action_doPreviewComment': 1} - }); + form.ajaxSubmit({ + success: function(response) { + var responseEl = $(response); + if(responseEl.is('form')) { + // Validation failed, renders form instead of single comment + form.find(".data-fields").replaceWith(responseEl.find(".data-fields")); + } else { + // Default behaviour + previewEl.removeClass('loading').find('.middleColumn').html(responseEl); + } + }, + data: {'action_doPreviewComment': 1} + }); }); + */ /** * Hide outdated preview on form changes */ + /* $(':input', form).on('change keydown', function() { - previewEl.removeClass('loading').hide(); - }); + previewEl.removeClass('loading').hide(); + });*/ /** * Clicking one of the metalinks performs the operation via ajax * this inclues the spam and approve links */ + /* @todo Migrate to work with nested comments commentsList.on('click', '.action-links a', function(e) { var link = $(this); var comment = link.parents('.comment:first'); @@ -149,11 +176,12 @@ e.preventDefault(); }); - + */ /** * Ajax pagination */ + /* @todo Migrate to work with nested comments pagination.find('a').on('click', function(){ commentsList.addClass('loading'); $.ajax({ @@ -165,14 +193,16 @@ pagination.hide().html(html.find('.comments-pagination:first').html()).fadeIn(); commentsList.removeClass('loading'); $('html, body').animate({ - scrollTop: commentsList.offset().top - 30 - }, 200); + scrollTop: commentsList.offset().top - 30 + }, 200); }, failure: function(html) { alert('Error loading comments'); } }); return false; - }); + });*/ }); })(jQuery); + + diff --git a/scss/comments.scss b/scss/comments.scss new file mode 100644 index 0000000..2d9eedd --- /dev/null +++ b/scss/comments.scss @@ -0,0 +1,190 @@ +// Colours +$blue: #00acee !default; +$blueDark: #0064cd !default; +$green: #46a546 !default; +$redLight: #DB4A39 !default; +$red: #9d261d !default; +$yellow: #ffc40d !default; +$orange: #f48b33 !default; +$pink: #c3325f !default; +$purple: #7a43b6 !default; + +#comments-holder { + clear: both; + + h2 { + margin: 30px 0 10px; + padding-bottom: 5px; + border-bottom: 1px solid #eee; + } + + h3 { + margin-top: 0; + } + + .field { + clear: left; + } + + .num, + .author { + font-size: 1.3em; + } + + .num { + color: #999; + margin-right: 5px; + } + + .num-total { + line-height: 40px; + margin-bottom: 0; + } + + .comments-list { + margin: 0; + } + + // A published comment + .comment { + clear:both; + list-style-type: none; + overflow: auto; + padding: 20px 0 10px; + position: relative; + + &.author-comment { + &:after { + content: 'Author'; + float: right; + position: absolute; + top: 1.5em; + right: 0; + font-size: 1em; + font-weight: bold; + color: $blue; + } + + .comment-text { + border: 1px solid $blue; + } + + } + + &.spam { + .comment-text { + border: 1px dashed $orange; + } + } + + .comment-text { + background-color: #fff; + border: 1px solid #ddd; + box-shadow: none; + margin: 0; + padding: 0 15% 0 20px; + white-space: pre; + white-space: pre-wrap; + white-space: pre-line; + word-wrap: break-word; + + p:last-child { + margin-bottom: 0; + } + } + + .date { + font-size: 16px; + + &:before { + content: '\0000a0\0000a0\0000a0\0000a0'; + } + } + + &.unmoderated { + border: 2px dashed gray; + margin: 2em 0 4em; + padding: 2em; + + .comment-moderation { + margin-bottom: 0; // Remove the margin to compensate for unmoderated comment padding. + } + } + + .info { + margin-bottom: 10px; + } + + &.spam .comment{ + border: 1px dashed $orange; + color: $orange; + border-radius: 4px; + padding: 2.5em 1em 1em; + } + + .comment-replies-container { + margin-left: 20px; + padding-left: 10px; + border-left: 1px dashed #999; + + .comment-reply-form-holder { + padding: 0 10px; // Prevent clipping issues on slideUp/Down + } + .comment-replies-holder { + padding: 0 0 0 10px; + } + } + } + + // Admin actions + .comment-moderation { + float: right; + margin: 1em 0 2em; + + .heading { + margin-bottom: 0; + } + } + + .action-links { + margin: 20px 0 10px; + + li { + display: inline; + list-style-type: none; + margin-left: 20px; + overflow: auto; + + &:first-child { + margin-left: 0; + } + + &.comment-reply-action { + float: right; + } + } + } + + .comment-count { + margin: 15px 0; + } + + // The comment form + .commenting-area { + margin-top: 50px; + + label.left{ + font-weight: normal; + } + } + + .commenting-rss-feed { + margin-top: 4em; + text-align: right; + } + + .no-comments-yet { + display: inline-block; + margin-top: 10px; + } +} diff --git a/templates/CommentReplies.ss b/templates/CommentReplies.ss new file mode 100644 index 0000000..0b8d9a4 --- /dev/null +++ b/templates/CommentReplies.ss @@ -0,0 +1,23 @@ +<% if $RepliesEnabled %> +
+ +
+ $ReplyForm +
+ +
+ <% if $Replies %> +
    + <% loop $Replies %> +
  • + <% include CommentsInterface_singlecomment %> +
  • + <% end_loop %> +
+ <% with $Replies %> + <% include ReplyPagination %> + <% end_with %> + <% end_if %> +
+
+<% end_if %> diff --git a/templates/CommentsInterface.ss b/templates/CommentsInterface.ss index 4577746..0cd141c 100755 --- a/templates/CommentsInterface.ss +++ b/templates/CommentsInterface.ss @@ -1,3 +1,5 @@ +<% require themedCSS('comments', 'comments') %> + <% if $CommentsEnabled %>

<% _t('CommentsInterface_ss.POSTCOM','Post your comment') %>

@@ -21,7 +23,7 @@
<% if $PagedComments %> -
    +
      <% loop $PagedComments %>
    • <% include CommentsInterface_singlecomment %> diff --git a/templates/CommentsInterface_singlecomment.ss b/templates/CommentsInterface_singlecomment.ss index acff806..c764005 100755 --- a/templates/CommentsInterface_singlecomment.ss +++ b/templates/CommentsInterface_singlecomment.ss @@ -1,18 +1,23 @@ -
      -<% if $Gravatar %>Gravatar for $Name<% end_if %> - $EscapedComment -
      - -<% if not isPreview %> +<% if not $isPreview %>

      <% if $URL %> - <% _t('CommentsInterface_singlecomment_ss.PBY','Posted by') %> $AuthorName.XML, $Created.Nice ($Created.Ago) + $AuthorName.XML <% else %> - <% _t('CommentsInterface_singlecomment_ss.PBY','Posted by') %> $AuthorName.XML, $Created.Nice ($Created.Ago) + $AuthorName.XML + <% end_if %> + $Created.Nice ($Created.Ago) + <% if $Gravatar %> + Gravatar for $Name.ATT <% end_if %>

      +<% end_if %> - <% if $ApproveLink || $SpamLink || $HamLink || $DeleteLink %> +
      +

      $EscapedComment

      +
      + +<% if not $isPreview %> + <% if $ApproveLink || $SpamLink || $HamLink || $DeleteLink || $RepliesEnabled %> <% end_if %> + + <% include CommentReplies %> <% end_if %> diff --git a/templates/Includes/ReplyPagination.ss b/templates/Includes/ReplyPagination.ss new file mode 100644 index 0000000..655f73a --- /dev/null +++ b/templates/Includes/ReplyPagination.ss @@ -0,0 +1 @@ +<% include CommentPagination %> diff --git a/tests/CommentingControllerTest.php b/tests/CommentingControllerTest.php new file mode 100644 index 0000000..94a09e7 --- /dev/null +++ b/tests/CommentingControllerTest.php @@ -0,0 +1,114 @@ +securityEnabled) { + SecurityToken::enable(); + } else { + SecurityToken::disable(); + } + parent::tearDown(); + } + + public function setUp() { + parent::setUp(); + $this->securityEnabled = SecurityToken::is_enabled(); + } + + public function testRSS() { + $item = $this->objFromFixture('CommentableItem', 'first'); + + // comments sitewide + $response = $this->get('CommentingController/rss'); + $this->assertEquals(10, substr_count($response->getBody(), ""), "10 approved, non spam comments on page 1"); + + $response = $this->get('CommentingController/rss?start=10'); + $this->assertEquals(4, substr_count($response->getBody(), ""), "3 approved, non spam comments on page 2"); + + // all comments on a type + $response = $this->get('CommentingController/rss/CommentableItem'); + $this->assertEquals(10, substr_count($response->getBody(), "")); + + $response = $this->get('CommentingController/rss/CommentableItem?start=10'); + $this->assertEquals(4, substr_count($response->getBody(), ""), "3 approved, non spam comments on page 2"); + + // specific page + $response = $this->get('CommentingController/rss/CommentableItem/'.$item->ID); + $this->assertEquals(1, substr_count($response->getBody(), "")); + $this->assertContains('FA', $response->getBody()); + + // test accessing comments on a type that doesn't exist + $response = $this->get('CommentingController/rss/Fake'); + $this->assertEquals(404, $response->getStatusCode()); + } + + public function testCommentsForm() { + SecurityToken::disable(); + $this->autoFollowRedirection = false; + $parent = $this->objFromFixture('CommentableItem', 'first'); + + // Test posting to base comment + $response = $this->post('CommentingController/CommentsForm', + array( + 'Name' => 'Poster', + 'Email' => 'guy@test.com', + 'Comment' => 'My Comment', + 'ParentID' => $parent->ID, + 'BaseClass' => 'CommentableItem', + 'action_doPostComment' => 'Post' + ) + ); + $this->assertEquals(302, $response->getStatusCode()); + $this->assertStringStartsWith('CommentableItem_Controller#comment-', $response->getHeader('Location')); + $this->assertDOSEquals( + array(array( + 'Name' => 'Poster', + 'Email' => 'guy@test.com', + 'Comment' => 'My Comment', + 'ParentID' => $parent->ID, + 'BaseClass' => 'CommentableItem', + )), + Comment::get()->filter('Email', 'guy@test.com') + ); + + // Test posting to parent comment + $parentComment = $this->objFromFixture('Comment', 'firstComA'); + $this->assertEquals(0, $parentComment->ChildComments()->count()); + + $response = $this->post( + 'CommentingController/reply/'.$parentComment->ID, + array( + 'Name' => 'Test Author', + 'Email' => 'test@test.com', + 'Comment' => 'Making a reply to firstComA', + 'ParentID' => $parent->ID, + 'BaseClass' => 'CommentableItem', + 'ParentCommentID' => $parentComment->ID, + 'action_doPostComment' => 'Post' + ) + ); + $this->assertEquals(302, $response->getStatusCode()); + $this->assertStringStartsWith('CommentableItem_Controller#comment-', $response->getHeader('Location')); + $this->assertDOSEquals(array(array( + 'Name' => 'Test Author', + 'Email' => 'test@test.com', + 'Comment' => 'Making a reply to firstComA', + 'ParentID' => $parent->ID, + 'BaseClass' => 'CommentableItem', + 'ParentCommentID' => $parentComment->ID + )), $parentComment->ChildComments()); + } +} diff --git a/tests/CommentingControllerTests.php b/tests/CommentingControllerTests.php deleted file mode 100644 index 8107bcc..0000000 --- a/tests/CommentingControllerTests.php +++ /dev/null @@ -1,59 +0,0 @@ -objFromFixture('CommentableItem', 'first'); - - // comments sitewide - $response = $this->get('CommentingController/rss'); - $this->assertEquals(10, substr_count($response->getBody(), ""), "10 approved, non spam comments on page 1"); - - $response = $this->get('CommentingController/rss?start=10'); - $this->assertEquals(4, substr_count($response->getBody(), ""), "3 approved, non spam comments on page 2"); - - // all comments on a type - $response = $this->get('CommentingController/rss/CommentableItem'); - $this->assertEquals(10, substr_count($response->getBody(), "")); - - $response = $this->get('CommentingController/rss/CommentableItem?start=10'); - $this->assertEquals(4, substr_count($response->getBody(), ""), "3 approved, non spam comments on page 2"); - - // specific page - $response = $this->get('CommentingController/rss/CommentableItem/'.$item->ID); - $this->assertEquals(1, substr_count($response->getBody(), "")); - $this->assertContains('FA', $response->getBody()); - - // test accessing comments on a type that doesn't exist - $response = $this->get('CommentingController/rss/Fake'); - $this->assertEquals(404, $response->getStatusCode()); - } - - public function testRSSSecuredCommentsForm() { - $this->markTestIncomplete("Not implemented"); - } - - public function testCommentsForm() { - $this->markTestIncomplete("Not implemented"); - } - - public function testDoCommentsForm() { - $this->markTestIncomplete("Not implemented"); - } -} diff --git a/thirdparty/jquery-validate/additional-methods.js b/thirdparty/jquery-validate/additional-methods.js new file mode 100644 index 0000000..e5d0640 --- /dev/null +++ b/thirdparty/jquery-validate/additional-methods.js @@ -0,0 +1,940 @@ +/*! + * jQuery Validation Plugin v1.13.1 + * + * http://jqueryvalidation.org/ + * + * Copyright (c) 2015 Jörn Zaefferer + * Released under the MIT license + */ +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + define( ["jquery", "./jquery.validate"], factory ); + } else { + factory( jQuery ); + } +}(function( $ ) { + +(function() { + + function stripHtml(value) { + // remove html tags and space chars + return value.replace(/<.[^<>]*?>/g, " ").replace(/ | /gi, " ") + // remove punctuation + .replace(/[.(),;:!?%#$'\"_+=\/\-“”’]*/g, ""); + } + + $.validator.addMethod("maxWords", function(value, element, params) { + return this.optional(element) || stripHtml(value).match(/\b\w+\b/g).length <= params; + }, $.validator.format("Please enter {0} words or less.")); + + $.validator.addMethod("minWords", function(value, element, params) { + return this.optional(element) || stripHtml(value).match(/\b\w+\b/g).length >= params; + }, $.validator.format("Please enter at least {0} words.")); + + $.validator.addMethod("rangeWords", function(value, element, params) { + var valueStripped = stripHtml(value), + regex = /\b\w+\b/g; + return this.optional(element) || valueStripped.match(regex).length >= params[0] && valueStripped.match(regex).length <= params[1]; + }, $.validator.format("Please enter between {0} and {1} words.")); + +}()); + +// Accept a value from a file input based on a required mimetype +$.validator.addMethod("accept", function(value, element, param) { + // Split mime on commas in case we have multiple types we can accept + var typeParam = typeof param === "string" ? param.replace(/\s/g, "").replace(/,/g, "|") : "image/*", + optionalValue = this.optional(element), + i, file; + + // Element is optional + if (optionalValue) { + return optionalValue; + } + + if ($(element).attr("type") === "file") { + // If we are using a wildcard, make it regex friendly + typeParam = typeParam.replace(/\*/g, ".*"); + + // Check if the element has a FileList before checking each file + if (element.files && element.files.length) { + for (i = 0; i < element.files.length; i++) { + file = element.files[i]; + + // Grab the mimetype from the loaded file, verify it matches + if (!file.type.match(new RegExp( ".?(" + typeParam + ")$", "i"))) { + return false; + } + } + } + } + + // Either return true because we've validated each file, or because the + // browser does not support element.files and the FileList feature + return true; +}, $.validator.format("Please enter a value with a valid mimetype.")); + +$.validator.addMethod("alphanumeric", function(value, element) { + return this.optional(element) || /^\w+$/i.test(value); +}, "Letters, numbers, and underscores only please"); + +/* + * Dutch bank account numbers (not 'giro' numbers) have 9 digits + * and pass the '11 check'. + * We accept the notation with spaces, as that is common. + * acceptable: 123456789 or 12 34 56 789 + */ +$.validator.addMethod("bankaccountNL", function(value, element) { + if (this.optional(element)) { + return true; + } + if (!(/^[0-9]{9}|([0-9]{2} ){3}[0-9]{3}$/.test(value))) { + return false; + } + // now '11 check' + var account = value.replace(/ /g, ""), // remove spaces + sum = 0, + len = account.length, + pos, factor, digit; + for ( pos = 0; pos < len; pos++ ) { + factor = len - pos; + digit = account.substring(pos, pos + 1); + sum = sum + factor * digit; + } + return sum % 11 === 0; +}, "Please specify a valid bank account number"); + +$.validator.addMethod("bankorgiroaccountNL", function(value, element) { + return this.optional(element) || + ($.validator.methods.bankaccountNL.call(this, value, element)) || + ($.validator.methods.giroaccountNL.call(this, value, element)); +}, "Please specify a valid bank or giro account number"); + +/** + * BIC is the business identifier code (ISO 9362). This BIC check is not a guarantee for authenticity. + * + * BIC pattern: BBBBCCLLbbb (8 or 11 characters long; bbb is optional) + * + * BIC definition in detail: + * - First 4 characters - bank code (only letters) + * - Next 2 characters - ISO 3166-1 alpha-2 country code (only letters) + * - Next 2 characters - location code (letters and digits) + * a. shall not start with '0' or '1' + * b. second character must be a letter ('O' is not allowed) or one of the following digits ('0' for test (therefore not allowed), '1' for passive participant and '2' for active participant) + * - Last 3 characters - branch code, optional (shall not start with 'X' except in case of 'XXX' for primary office) (letters and digits) + */ +$.validator.addMethod("bic", function(value, element) { + return this.optional( element ) || /^([A-Z]{6}[A-Z2-9][A-NP-Z1-2])(X{3}|[A-WY-Z0-9][A-Z0-9]{2})?$/.test( value ); +}, "Please specify a valid BIC code"); + +/* + * Código de identificación fiscal ( CIF ) is the tax identification code for Spanish legal entities + * Further rules can be found in Spanish on http://es.wikipedia.org/wiki/C%C3%B3digo_de_identificaci%C3%B3n_fiscal + */ +$.validator.addMethod( "cifES", function( value ) { + "use strict"; + + var num = [], + controlDigit, sum, i, count, tmp, secondDigit; + + value = value.toUpperCase(); + + // Quick format test + if ( !value.match( "((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)" ) ) { + return false; + } + + for ( i = 0; i < 9; i++ ) { + num[ i ] = parseInt( value.charAt( i ), 10 ); + } + + // Algorithm for checking CIF codes + sum = num[ 2 ] + num[ 4 ] + num[ 6 ]; + for ( count = 1; count < 8; count += 2 ) { + tmp = ( 2 * num[ count ] ).toString(); + secondDigit = tmp.charAt( 1 ); + + sum += parseInt( tmp.charAt( 0 ), 10 ) + ( secondDigit === "" ? 0 : parseInt( secondDigit, 10 ) ); + } + + /* The first (position 1) is a letter following the following criteria: + * A. Corporations + * B. LLCs + * C. General partnerships + * D. Companies limited partnerships + * E. Communities of goods + * F. Cooperative Societies + * G. Associations + * H. Communities of homeowners in horizontal property regime + * J. Civil Societies + * K. Old format + * L. Old format + * M. Old format + * N. Nonresident entities + * P. Local authorities + * Q. Autonomous bodies, state or not, and the like, and congregations and religious institutions + * R. Congregations and religious institutions (since 2008 ORDER EHA/451/2008) + * S. Organs of State Administration and regions + * V. Agrarian Transformation + * W. Permanent establishments of non-resident in Spain + */ + if ( /^[ABCDEFGHJNPQRSUVW]{1}/.test( value ) ) { + sum += ""; + controlDigit = 10 - parseInt( sum.charAt( sum.length - 1 ), 10 ); + value += controlDigit; + return ( num[ 8 ].toString() === String.fromCharCode( 64 + controlDigit ) || num[ 8 ].toString() === value.charAt( value.length - 1 ) ); + } + + return false; + +}, "Please specify a valid CIF number." ); + +/* NOTICE: Modified version of Castle.Components.Validator.CreditCardValidator + * Redistributed under the the Apache License 2.0 at http://www.apache.org/licenses/LICENSE-2.0 + * Valid Types: mastercard, visa, amex, dinersclub, enroute, discover, jcb, unknown, all (overrides all other settings) + */ +$.validator.addMethod("creditcardtypes", function(value, element, param) { + if (/[^0-9\-]+/.test(value)) { + return false; + } + + value = value.replace(/\D/g, ""); + + var validTypes = 0x0000; + + if (param.mastercard) { + validTypes |= 0x0001; + } + if (param.visa) { + validTypes |= 0x0002; + } + if (param.amex) { + validTypes |= 0x0004; + } + if (param.dinersclub) { + validTypes |= 0x0008; + } + if (param.enroute) { + validTypes |= 0x0010; + } + if (param.discover) { + validTypes |= 0x0020; + } + if (param.jcb) { + validTypes |= 0x0040; + } + if (param.unknown) { + validTypes |= 0x0080; + } + if (param.all) { + validTypes = 0x0001 | 0x0002 | 0x0004 | 0x0008 | 0x0010 | 0x0020 | 0x0040 | 0x0080; + } + if (validTypes & 0x0001 && /^(5[12345])/.test(value)) { //mastercard + return value.length === 16; + } + if (validTypes & 0x0002 && /^(4)/.test(value)) { //visa + return value.length === 16; + } + if (validTypes & 0x0004 && /^(3[47])/.test(value)) { //amex + return value.length === 15; + } + if (validTypes & 0x0008 && /^(3(0[012345]|[68]))/.test(value)) { //dinersclub + return value.length === 14; + } + if (validTypes & 0x0010 && /^(2(014|149))/.test(value)) { //enroute + return value.length === 15; + } + if (validTypes & 0x0020 && /^(6011)/.test(value)) { //discover + return value.length === 16; + } + if (validTypes & 0x0040 && /^(3)/.test(value)) { //jcb + return value.length === 16; + } + if (validTypes & 0x0040 && /^(2131|1800)/.test(value)) { //jcb + return value.length === 15; + } + if (validTypes & 0x0080) { //unknown + return true; + } + return false; +}, "Please enter a valid credit card number."); + +/** + * Validates currencies with any given symbols by @jameslouiz + * Symbols can be optional or required. Symbols required by default + * + * Usage examples: + * currency: ["£", false] - Use false for soft currency validation + * currency: ["$", false] + * currency: ["RM", false] - also works with text based symbols such as "RM" - Malaysia Ringgit etc + * + * + * + * Soft symbol checking + * currencyInput: { + * currency: ["$", false] + * } + * + * Strict symbol checking (default) + * currencyInput: { + * currency: "$" + * //OR + * currency: ["$", true] + * } + * + * Multiple Symbols + * currencyInput: { + * currency: "$,£,¢" + * } + */ +$.validator.addMethod("currency", function(value, element, param) { + var isParamString = typeof param === "string", + symbol = isParamString ? param : param[0], + soft = isParamString ? true : param[1], + regex; + + symbol = symbol.replace(/,/g, ""); + symbol = soft ? symbol + "]" : symbol + "]?"; + regex = "^[" + symbol + "([1-9]{1}[0-9]{0,2}(\\,[0-9]{3})*(\\.[0-9]{0,2})?|[1-9]{1}[0-9]{0,}(\\.[0-9]{0,2})?|0(\\.[0-9]{0,2})?|(\\.[0-9]{1,2})?)$"; + regex = new RegExp(regex); + return this.optional(element) || regex.test(value); + +}, "Please specify a valid currency"); + +$.validator.addMethod("dateFA", function(value, element) { + return this.optional(element) || /^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test(value); +}, "Please enter a correct date"); + +/** + * Return true, if the value is a valid date, also making this formal check dd/mm/yyyy. + * + * @example $.validator.methods.date("01/01/1900") + * @result true + * + * @example $.validator.methods.date("01/13/1990") + * @result false + * + * @example $.validator.methods.date("01.01.1900") + * @result false + * + * @example + * @desc Declares an optional input element whose value must be a valid date. + * + * @name $.validator.methods.dateITA + * @type Boolean + * @cat Plugins/Validate/Methods + */ +$.validator.addMethod("dateITA", function(value, element) { + var check = false, + re = /^\d{1,2}\/\d{1,2}\/\d{4}$/, + adata, gg, mm, aaaa, xdata; + if ( re.test(value)) { + adata = value.split("/"); + gg = parseInt(adata[0], 10); + mm = parseInt(adata[1], 10); + aaaa = parseInt(adata[2], 10); + xdata = new Date(Date.UTC(aaaa, mm - 1, gg, 12, 0, 0, 0)); + if ( ( xdata.getUTCFullYear() === aaaa ) && ( xdata.getUTCMonth () === mm - 1 ) && ( xdata.getUTCDate() === gg ) ) { + check = true; + } else { + check = false; + } + } else { + check = false; + } + return this.optional(element) || check; +}, "Please enter a correct date"); + +$.validator.addMethod("dateNL", function(value, element) { + return this.optional(element) || /^(0?[1-9]|[12]\d|3[01])[\.\/\-](0?[1-9]|1[012])[\.\/\-]([12]\d)?(\d\d)$/.test(value); +}, "Please enter a correct date"); + +// Older "accept" file extension method. Old docs: http://docs.jquery.com/Plugins/Validation/Methods/accept +$.validator.addMethod("extension", function(value, element, param) { + param = typeof param === "string" ? param.replace(/,/g, "|") : "png|jpe?g|gif"; + return this.optional(element) || value.match(new RegExp(".(" + param + ")$", "i")); +}, $.validator.format("Please enter a value with a valid extension.")); + +/** + * Dutch giro account numbers (not bank numbers) have max 7 digits + */ +$.validator.addMethod("giroaccountNL", function(value, element) { + return this.optional(element) || /^[0-9]{1,7}$/.test(value); +}, "Please specify a valid giro account number"); + +/** + * IBAN is the international bank account number. + * It has a country - specific format, that is checked here too + */ +$.validator.addMethod("iban", function(value, element) { + // some quick simple tests to prevent needless work + if (this.optional(element)) { + return true; + } + + // remove spaces and to upper case + var iban = value.replace(/ /g, "").toUpperCase(), + ibancheckdigits = "", + leadingZeroes = true, + cRest = "", + cOperator = "", + countrycode, ibancheck, charAt, cChar, bbanpattern, bbancountrypatterns, ibanregexp, i, p; + + if (!(/^([a-zA-Z0-9]{4} ){2,8}[a-zA-Z0-9]{1,4}|[a-zA-Z0-9]{12,34}$/.test(iban))) { + return false; + } + + // check the country code and find the country specific format + countrycode = iban.substring(0, 2); + bbancountrypatterns = { + "AL": "\\d{8}[\\dA-Z]{16}", + "AD": "\\d{8}[\\dA-Z]{12}", + "AT": "\\d{16}", + "AZ": "[\\dA-Z]{4}\\d{20}", + "BE": "\\d{12}", + "BH": "[A-Z]{4}[\\dA-Z]{14}", + "BA": "\\d{16}", + "BR": "\\d{23}[A-Z][\\dA-Z]", + "BG": "[A-Z]{4}\\d{6}[\\dA-Z]{8}", + "CR": "\\d{17}", + "HR": "\\d{17}", + "CY": "\\d{8}[\\dA-Z]{16}", + "CZ": "\\d{20}", + "DK": "\\d{14}", + "DO": "[A-Z]{4}\\d{20}", + "EE": "\\d{16}", + "FO": "\\d{14}", + "FI": "\\d{14}", + "FR": "\\d{10}[\\dA-Z]{11}\\d{2}", + "GE": "[\\dA-Z]{2}\\d{16}", + "DE": "\\d{18}", + "GI": "[A-Z]{4}[\\dA-Z]{15}", + "GR": "\\d{7}[\\dA-Z]{16}", + "GL": "\\d{14}", + "GT": "[\\dA-Z]{4}[\\dA-Z]{20}", + "HU": "\\d{24}", + "IS": "\\d{22}", + "IE": "[\\dA-Z]{4}\\d{14}", + "IL": "\\d{19}", + "IT": "[A-Z]\\d{10}[\\dA-Z]{12}", + "KZ": "\\d{3}[\\dA-Z]{13}", + "KW": "[A-Z]{4}[\\dA-Z]{22}", + "LV": "[A-Z]{4}[\\dA-Z]{13}", + "LB": "\\d{4}[\\dA-Z]{20}", + "LI": "\\d{5}[\\dA-Z]{12}", + "LT": "\\d{16}", + "LU": "\\d{3}[\\dA-Z]{13}", + "MK": "\\d{3}[\\dA-Z]{10}\\d{2}", + "MT": "[A-Z]{4}\\d{5}[\\dA-Z]{18}", + "MR": "\\d{23}", + "MU": "[A-Z]{4}\\d{19}[A-Z]{3}", + "MC": "\\d{10}[\\dA-Z]{11}\\d{2}", + "MD": "[\\dA-Z]{2}\\d{18}", + "ME": "\\d{18}", + "NL": "[A-Z]{4}\\d{10}", + "NO": "\\d{11}", + "PK": "[\\dA-Z]{4}\\d{16}", + "PS": "[\\dA-Z]{4}\\d{21}", + "PL": "\\d{24}", + "PT": "\\d{21}", + "RO": "[A-Z]{4}[\\dA-Z]{16}", + "SM": "[A-Z]\\d{10}[\\dA-Z]{12}", + "SA": "\\d{2}[\\dA-Z]{18}", + "RS": "\\d{18}", + "SK": "\\d{20}", + "SI": "\\d{15}", + "ES": "\\d{20}", + "SE": "\\d{20}", + "CH": "\\d{5}[\\dA-Z]{12}", + "TN": "\\d{20}", + "TR": "\\d{5}[\\dA-Z]{17}", + "AE": "\\d{3}\\d{16}", + "GB": "[A-Z]{4}\\d{14}", + "VG": "[\\dA-Z]{4}\\d{16}" + }; + + bbanpattern = bbancountrypatterns[countrycode]; + // As new countries will start using IBAN in the + // future, we only check if the countrycode is known. + // This prevents false negatives, while almost all + // false positives introduced by this, will be caught + // by the checksum validation below anyway. + // Strict checking should return FALSE for unknown + // countries. + if (typeof bbanpattern !== "undefined") { + ibanregexp = new RegExp("^[A-Z]{2}\\d{2}" + bbanpattern + "$", ""); + if (!(ibanregexp.test(iban))) { + return false; // invalid country specific format + } + } + + // now check the checksum, first convert to digits + ibancheck = iban.substring(4, iban.length) + iban.substring(0, 4); + for (i = 0; i < ibancheck.length; i++) { + charAt = ibancheck.charAt(i); + if (charAt !== "0") { + leadingZeroes = false; + } + if (!leadingZeroes) { + ibancheckdigits += "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(charAt); + } + } + + // calculate the result of: ibancheckdigits % 97 + for (p = 0; p < ibancheckdigits.length; p++) { + cChar = ibancheckdigits.charAt(p); + cOperator = "" + cRest + "" + cChar; + cRest = cOperator % 97; + } + return cRest === 1; +}, "Please specify a valid IBAN"); + +$.validator.addMethod("integer", function(value, element) { + return this.optional(element) || /^-?\d+$/.test(value); +}, "A positive or negative non-decimal number please"); + +$.validator.addMethod("ipv4", function(value, element) { + return this.optional(element) || /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/i.test(value); +}, "Please enter a valid IP v4 address."); + +$.validator.addMethod("ipv6", function(value, element) { + return this.optional(element) || /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i.test(value); +}, "Please enter a valid IP v6 address."); + +$.validator.addMethod("lettersonly", function(value, element) { + return this.optional(element) || /^[a-z]+$/i.test(value); +}, "Letters only please"); + +$.validator.addMethod("letterswithbasicpunc", function(value, element) { + return this.optional(element) || /^[a-z\-.,()'"\s]+$/i.test(value); +}, "Letters or punctuation only please"); + +$.validator.addMethod("mobileNL", function(value, element) { + return this.optional(element) || /^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)6((\s|\s?\-\s?)?[0-9]){8}$/.test(value); +}, "Please specify a valid mobile number"); + +/* For UK phone functions, do the following server side processing: + * Compare original input with this RegEx pattern: + * ^\(?(?:(?:00\)?[\s\-]?\(?|\+)(44)\)?[\s\-]?\(?(?:0\)?[\s\-]?\(?)?|0)([1-9]\d{1,4}\)?[\s\d\-]+)$ + * Extract $1 and set $prefix to '+44' if $1 is '44', otherwise set $prefix to '0' + * Extract $2 and remove hyphens, spaces and parentheses. Phone number is combined $prefix and $2. + * A number of very detailed GB telephone number RegEx patterns can also be found at: + * http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_GB_Telephone_Numbers + */ +$.validator.addMethod("mobileUK", function(phone_number, element) { + phone_number = phone_number.replace(/\(|\)|\s+|-/g, ""); + return this.optional(element) || phone_number.length > 9 && + phone_number.match(/^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/); +}, "Please specify a valid mobile number"); + +/* + * The número de identidad de extranjero ( NIE )is a code used to identify the non-nationals in Spain + */ +$.validator.addMethod( "nieES", function( value ) { + "use strict"; + + value = value.toUpperCase(); + + // Basic format test + if ( !value.match( "((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)" ) ) { + return false; + } + + // Test NIE + //T + if ( /^[T]{1}/.test( value ) ) { + return ( value[ 8 ] === /^[T]{1}[A-Z0-9]{8}$/.test( value ) ); + } + + //XYZ + if ( /^[XYZ]{1}/.test( value ) ) { + return ( + value[ 8 ] === "TRWAGMYFPDXBNJZSQVHLCKE".charAt( + value.replace( "X", "0" ) + .replace( "Y", "1" ) + .replace( "Z", "2" ) + .substring( 0, 8 ) % 23 + ) + ); + } + + return false; + +}, "Please specify a valid NIE number." ); + +/* + * The Número de Identificación Fiscal ( NIF ) is the way tax identification used in Spain for individuals + */ +$.validator.addMethod( "nifES", function( value ) { + "use strict"; + + value = value.toUpperCase(); + + // Basic format test + if ( !value.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)") ) { + return false; + } + + // Test NIF + if ( /^[0-9]{8}[A-Z]{1}$/.test( value ) ) { + return ( "TRWAGMYFPDXBNJZSQVHLCKE".charAt( value.substring( 8, 0 ) % 23 ) === value.charAt( 8 ) ); + } + // Test specials NIF (starts with K, L or M) + if ( /^[KLM]{1}/.test( value ) ) { + return ( value[ 8 ] === String.fromCharCode( 64 ) ); + } + + return false; + +}, "Please specify a valid NIF number." ); + +$.validator.addMethod("nowhitespace", function(value, element) { + return this.optional(element) || /^\S+$/i.test(value); +}, "No white space please"); + +/** +* Return true if the field value matches the given format RegExp +* +* @example $.validator.methods.pattern("AR1004",element,/^AR\d{4}$/) +* @result true +* +* @example $.validator.methods.pattern("BR1004",element,/^AR\d{4}$/) +* @result false +* +* @name $.validator.methods.pattern +* @type Boolean +* @cat Plugins/Validate/Methods +*/ +$.validator.addMethod("pattern", function(value, element, param) { + if (this.optional(element)) { + return true; + } + if (typeof param === "string") { + param = new RegExp("^(?:" + param + ")$"); + } + return param.test(value); +}, "Invalid format."); + +/** + * Dutch phone numbers have 10 digits (or 11 and start with +31). + */ +$.validator.addMethod("phoneNL", function(value, element) { + return this.optional(element) || /^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test(value); +}, "Please specify a valid phone number."); + +/* For UK phone functions, do the following server side processing: + * Compare original input with this RegEx pattern: + * ^\(?(?:(?:00\)?[\s\-]?\(?|\+)(44)\)?[\s\-]?\(?(?:0\)?[\s\-]?\(?)?|0)([1-9]\d{1,4}\)?[\s\d\-]+)$ + * Extract $1 and set $prefix to '+44' if $1 is '44', otherwise set $prefix to '0' + * Extract $2 and remove hyphens, spaces and parentheses. Phone number is combined $prefix and $2. + * A number of very detailed GB telephone number RegEx patterns can also be found at: + * http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_GB_Telephone_Numbers + */ +$.validator.addMethod("phoneUK", function(phone_number, element) { + phone_number = phone_number.replace(/\(|\)|\s+|-/g, ""); + return this.optional(element) || phone_number.length > 9 && + phone_number.match(/^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/); +}, "Please specify a valid phone number"); + +/** + * matches US phone number format + * + * where the area code may not start with 1 and the prefix may not start with 1 + * allows '-' or ' ' as a separator and allows parens around area code + * some people may want to put a '1' in front of their number + * + * 1(212)-999-2345 or + * 212 999 2344 or + * 212-999-0983 + * + * but not + * 111-123-5434 + * and not + * 212 123 4567 + */ +$.validator.addMethod("phoneUS", function(phone_number, element) { + phone_number = phone_number.replace(/\s+/g, ""); + return this.optional(element) || phone_number.length > 9 && + phone_number.match(/^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/); +}, "Please specify a valid phone number"); + +/* For UK phone functions, do the following server side processing: + * Compare original input with this RegEx pattern: + * ^\(?(?:(?:00\)?[\s\-]?\(?|\+)(44)\)?[\s\-]?\(?(?:0\)?[\s\-]?\(?)?|0)([1-9]\d{1,4}\)?[\s\d\-]+)$ + * Extract $1 and set $prefix to '+44' if $1 is '44', otherwise set $prefix to '0' + * Extract $2 and remove hyphens, spaces and parentheses. Phone number is combined $prefix and $2. + * A number of very detailed GB telephone number RegEx patterns can also be found at: + * http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_GB_Telephone_Numbers + */ +//Matches UK landline + mobile, accepting only 01-3 for landline or 07 for mobile to exclude many premium numbers +$.validator.addMethod("phonesUK", function(phone_number, element) { + phone_number = phone_number.replace(/\(|\)|\s+|-/g, ""); + return this.optional(element) || phone_number.length > 9 && + phone_number.match(/^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/); +}, "Please specify a valid uk phone number"); + +/** + * Matches a valid Canadian Postal Code + * + * @example jQuery.validator.methods.postalCodeCA( "H0H 0H0", element ) + * @result true + * + * @example jQuery.validator.methods.postalCodeCA( "H0H0H0", element ) + * @result false + * + * @name jQuery.validator.methods.postalCodeCA + * @type Boolean + * @cat Plugins/Validate/Methods + */ +$.validator.addMethod( "postalCodeCA", function( value, element ) { + return this.optional( element ) || /^[ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d$/.test( value ); +}, "Please specify a valid postal code" ); + +/* +* Valida CEPs do brasileiros: +* +* Formatos aceitos: +* 99999-999 +* 99.999-999 +* 99999999 +*/ +$.validator.addMethod("postalcodeBR", function(cep_value, element) { + return this.optional(element) || /^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test( cep_value ); +}, "Informe um CEP válido."); + +/* Matches Italian postcode (CAP) */ +$.validator.addMethod("postalcodeIT", function(value, element) { + return this.optional(element) || /^\d{5}$/.test(value); +}, "Please specify a valid postal code"); + +$.validator.addMethod("postalcodeNL", function(value, element) { + return this.optional(element) || /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(value); +}, "Please specify a valid postal code"); + +// Matches UK postcode. Does not match to UK Channel Islands that have their own postcodes (non standard UK) +$.validator.addMethod("postcodeUK", function(value, element) { + return this.optional(element) || /^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test(value); +}, "Please specify a valid UK postcode"); + +/* + * Lets you say "at least X inputs that match selector Y must be filled." + * + * The end result is that neither of these inputs: + * + * + * + * + * ...will validate unless at least one of them is filled. + * + * partnumber: {require_from_group: [1,".productinfo"]}, + * description: {require_from_group: [1,".productinfo"]} + * + * options[0]: number of fields that must be filled in the group + * options[1]: CSS selector that defines the group of conditionally required fields + */ +$.validator.addMethod("require_from_group", function(value, element, options) { + var $fields = $(options[1], element.form), + $fieldsFirst = $fields.eq(0), + validator = $fieldsFirst.data("valid_req_grp") ? $fieldsFirst.data("valid_req_grp") : $.extend({}, this), + isValid = $fields.filter(function() { + return validator.elementValue(this); + }).length >= options[0]; + + // Store the cloned validator for future validation + $fieldsFirst.data("valid_req_grp", validator); + + // If element isn't being validated, run each require_from_group field's validation rules + if (!$(element).data("being_validated")) { + $fields.data("being_validated", true); + $fields.each(function() { + validator.element(this); + }); + $fields.data("being_validated", false); + } + return isValid; +}, $.validator.format("Please fill at least {0} of these fields.")); + +/* + * Lets you say "either at least X inputs that match selector Y must be filled, + * OR they must all be skipped (left blank)." + * + * The end result, is that none of these inputs: + * + * + * + * + * + * ...will validate unless either at least two of them are filled, + * OR none of them are. + * + * partnumber: {skip_or_fill_minimum: [2,".productinfo"]}, + * description: {skip_or_fill_minimum: [2,".productinfo"]}, + * color: {skip_or_fill_minimum: [2,".productinfo"]} + * + * options[0]: number of fields that must be filled in the group + * options[1]: CSS selector that defines the group of conditionally required fields + * + */ +$.validator.addMethod("skip_or_fill_minimum", function(value, element, options) { + var $fields = $(options[1], element.form), + $fieldsFirst = $fields.eq(0), + validator = $fieldsFirst.data("valid_skip") ? $fieldsFirst.data("valid_skip") : $.extend({}, this), + numberFilled = $fields.filter(function() { + return validator.elementValue(this); + }).length, + isValid = numberFilled === 0 || numberFilled >= options[0]; + + // Store the cloned validator for future validation + $fieldsFirst.data("valid_skip", validator); + + // If element isn't being validated, run each skip_or_fill_minimum field's validation rules + if (!$(element).data("being_validated")) { + $fields.data("being_validated", true); + $fields.each(function() { + validator.element(this); + }); + $fields.data("being_validated", false); + } + return isValid; +}, $.validator.format("Please either skip these fields or fill at least {0} of them.")); + +/* Validates US States and/or Territories by @jdforsythe + * Can be case insensitive or require capitalization - default is case insensitive + * Can include US Territories or not - default does not + * Can include US Military postal abbreviations (AA, AE, AP) - default does not + * + * Note: "States" always includes DC (District of Colombia) + * + * Usage examples: + * + * This is the default - case insensitive, no territories, no military zones + * stateInput: { + * caseSensitive: false, + * includeTerritories: false, + * includeMilitary: false + * } + * + * Only allow capital letters, no territories, no military zones + * stateInput: { + * caseSensitive: false + * } + * + * Case insensitive, include territories but not military zones + * stateInput: { + * includeTerritories: true + * } + * + * Only allow capital letters, include territories and military zones + * stateInput: { + * caseSensitive: true, + * includeTerritories: true, + * includeMilitary: true + * } + * + * + * + */ + +jQuery.validator.addMethod("stateUS", function(value, element, options) { + var isDefault = typeof options === "undefined", + caseSensitive = ( isDefault || typeof options.caseSensitive === "undefined" ) ? false : options.caseSensitive, + includeTerritories = ( isDefault || typeof options.includeTerritories === "undefined" ) ? false : options.includeTerritories, + includeMilitary = ( isDefault || typeof options.includeMilitary === "undefined" ) ? false : options.includeMilitary, + regex; + + if (!includeTerritories && !includeMilitary) { + regex = "^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$"; + } else if (includeTerritories && includeMilitary) { + regex = "^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$"; + } else if (includeTerritories) { + regex = "^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$"; + } else { + regex = "^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$"; + } + + regex = caseSensitive ? new RegExp(regex) : new RegExp(regex, "i"); + return this.optional(element) || regex.test(value); +}, +"Please specify a valid state"); + +// TODO check if value starts with <, otherwise don't try stripping anything +$.validator.addMethod("strippedminlength", function(value, element, param) { + return $(value).text().length >= param; +}, $.validator.format("Please enter at least {0} characters")); + +$.validator.addMethod("time", function(value, element) { + return this.optional(element) || /^([01]\d|2[0-3])(:[0-5]\d){1,2}$/.test(value); +}, "Please enter a valid time, between 00:00 and 23:59"); + +$.validator.addMethod("time12h", function(value, element) { + return this.optional(element) || /^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(value); +}, "Please enter a valid time in 12-hour am/pm format"); + +// same as url, but TLD is optional +$.validator.addMethod("url2", function(value, element) { + return this.optional(element) || /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value); +}, $.validator.messages.url); + +/** + * Return true, if the value is a valid vehicle identification number (VIN). + * + * Works with all kind of text inputs. + * + * @example + * @desc Declares a required input element whose value must be a valid vehicle identification number. + * + * @name $.validator.methods.vinUS + * @type Boolean + * @cat Plugins/Validate/Methods + */ +$.validator.addMethod("vinUS", function(v) { + if (v.length !== 17) { + return false; + } + + var LL = [ "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ], + VL = [ 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 7, 9, 2, 3, 4, 5, 6, 7, 8, 9 ], + FL = [ 8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2 ], + rs = 0, + i, n, d, f, cd, cdv; + + for (i = 0; i < 17; i++) { + f = FL[i]; + d = v.slice(i, i + 1); + if (i === 8) { + cdv = d; + } + if (!isNaN(d)) { + d *= f; + } else { + for (n = 0; n < LL.length; n++) { + if (d.toUpperCase() === LL[n]) { + d = VL[n]; + d *= f; + if (isNaN(cdv) && n === 8) { + cdv = LL[n]; + } + break; + } + } + } + rs += d; + } + cd = rs % 11; + if (cd === 10) { + cd = "X"; + } + if (cd === cdv) { + return true; + } + return false; +}, "The specified vehicle identification number (VIN) is invalid."); + +$.validator.addMethod("zipcodeUS", function(value, element) { + return this.optional(element) || /^\d{5}(-\d{4})?$/.test(value); +}, "The specified US ZIP Code is invalid"); + +$.validator.addMethod("ziprange", function(value, element) { + return this.optional(element) || /^90[2-5]\d\{2\}-\d{4}$/.test(value); +}, "Your ZIP-code must be in the range 902xx-xxxx to 905xx-xxxx"); + +})); \ No newline at end of file diff --git a/thirdparty/jquery-validate/additional-methods.min.js b/thirdparty/jquery-validate/additional-methods.min.js new file mode 100644 index 0000000..6e6f0e9 --- /dev/null +++ b/thirdparty/jquery-validate/additional-methods.min.js @@ -0,0 +1,4 @@ +/*! jQuery Validation Plugin - v1.13.1 - 10/14/2014 + * http://jqueryvalidation.org/ + * Copyright (c) 2014 Jörn Zaefferer; Licensed MIT */ +!function(a){"function"==typeof define&&define.amd?define(["jquery","./jquery.validate.min"],a):a(jQuery)}(function(a){!function(){function b(a){return a.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," ").replace(/[.(),;:!?%#$'\"_+=\/\-“”’]*/g,"")}a.validator.addMethod("maxWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length<=d},a.validator.format("Please enter {0} words or less.")),a.validator.addMethod("minWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length>=d},a.validator.format("Please enter at least {0} words.")),a.validator.addMethod("rangeWords",function(a,c,d){var e=b(a),f=/\b\w+\b/g;return this.optional(c)||e.match(f).length>=d[0]&&e.match(f).length<=d[1]},a.validator.format("Please enter between {0} and {1} words."))}(),a.validator.addMethod("accept",function(b,c,d){var e,f,g="string"==typeof d?d.replace(/\s/g,"").replace(/,/g,"|"):"image/*",h=this.optional(c);if(h)return h;if("file"===a(c).attr("type")&&(g=g.replace(/\*/g,".*"),c.files&&c.files.length))for(e=0;ec;c++)d=h-c,e=f.substring(c,c+1),g+=d*e;return g%11===0},"Please specify a valid bank account number"),a.validator.addMethod("bankorgiroaccountNL",function(b,c){return this.optional(c)||a.validator.methods.bankaccountNL.call(this,b,c)||a.validator.methods.giroaccountNL.call(this,b,c)},"Please specify a valid bank or giro account number"),a.validator.addMethod("bic",function(a,b){return this.optional(b)||/^([A-Z]{6}[A-Z2-9][A-NP-Z1-2])(X{3}|[A-WY-Z0-9][A-Z0-9]{2})?$/.test(a)},"Please specify a valid BIC code"),a.validator.addMethod("cifES",function(a){"use strict";var b,c,d,e,f,g,h=[];if(a=a.toUpperCase(),!a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)"))return!1;for(d=0;9>d;d++)h[d]=parseInt(a.charAt(d),10);for(c=h[2]+h[4]+h[6],e=1;8>e;e+=2)f=(2*h[e]).toString(),g=f.charAt(1),c+=parseInt(f.charAt(0),10)+(""===g?0:parseInt(g,10));return/^[ABCDEFGHJNPQRSUVW]{1}/.test(a)?(c+="",b=10-parseInt(c.charAt(c.length-1),10),a+=b,h[8].toString()===String.fromCharCode(64+b)||h[8].toString()===a.charAt(a.length-1)):!1},"Please specify a valid CIF number."),a.validator.addMethod("creditcardtypes",function(a,b,c){if(/[^0-9\-]+/.test(a))return!1;a=a.replace(/\D/g,"");var d=0;return c.mastercard&&(d|=1),c.visa&&(d|=2),c.amex&&(d|=4),c.dinersclub&&(d|=8),c.enroute&&(d|=16),c.discover&&(d|=32),c.jcb&&(d|=64),c.unknown&&(d|=128),c.all&&(d=255),1&d&&/^(5[12345])/.test(a)?16===a.length:2&d&&/^(4)/.test(a)?16===a.length:4&d&&/^(3[47])/.test(a)?15===a.length:8&d&&/^(3(0[012345]|[68]))/.test(a)?14===a.length:16&d&&/^(2(014|149))/.test(a)?15===a.length:32&d&&/^(6011)/.test(a)?16===a.length:64&d&&/^(3)/.test(a)?16===a.length:64&d&&/^(2131|1800)/.test(a)?15===a.length:128&d?!0:!1},"Please enter a valid credit card number."),a.validator.addMethod("currency",function(a,b,c){var d,e="string"==typeof c,f=e?c:c[0],g=e?!0:c[1];return f=f.replace(/,/g,""),f=g?f+"]":f+"]?",d="^["+f+"([1-9]{1}[0-9]{0,2}(\\,[0-9]{3})*(\\.[0-9]{0,2})?|[1-9]{1}[0-9]{0,}(\\.[0-9]{0,2})?|0(\\.[0-9]{0,2})?|(\\.[0-9]{1,2})?)$",d=new RegExp(d),this.optional(b)||d.test(a)},"Please specify a valid currency"),a.validator.addMethod("dateFA",function(a,b){return this.optional(b)||/^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test(a)},"Please enter a correct date"),a.validator.addMethod("dateITA",function(a,b){var c,d,e,f,g,h=!1,i=/^\d{1,2}\/\d{1,2}\/\d{4}$/;return i.test(a)?(c=a.split("/"),d=parseInt(c[0],10),e=parseInt(c[1],10),f=parseInt(c[2],10),g=new Date(f,e-1,d,12,0,0,0),h=g.getUTCFullYear()===f&&g.getUTCMonth()===e-1&&g.getUTCDate()===d?!0:!1):h=!1,this.optional(b)||h},"Please enter a correct date"),a.validator.addMethod("dateNL",function(a,b){return this.optional(b)||/^(0?[1-9]|[12]\d|3[01])[\.\/\-](0?[1-9]|1[012])[\.\/\-]([12]\d)?(\d\d)$/.test(a)},"Please enter a correct date"),a.validator.addMethod("extension",function(a,b,c){return c="string"==typeof c?c.replace(/,/g,"|"):"png|jpe?g|gif",this.optional(b)||a.match(new RegExp(".("+c+")$","i"))},a.validator.format("Please enter a value with a valid extension.")),a.validator.addMethod("giroaccountNL",function(a,b){return this.optional(b)||/^[0-9]{1,7}$/.test(a)},"Please specify a valid giro account number"),a.validator.addMethod("iban",function(a,b){if(this.optional(b))return!0;var c,d,e,f,g,h,i,j,k,l=a.replace(/ /g,"").toUpperCase(),m="",n=!0,o="",p="";if(!/^([a-zA-Z0-9]{4} ){2,8}[a-zA-Z0-9]{1,4}|[a-zA-Z0-9]{12,34}$/.test(l))return!1;if(c=l.substring(0,2),h={AL:"\\d{8}[\\dA-Z]{16}",AD:"\\d{8}[\\dA-Z]{12}",AT:"\\d{16}",AZ:"[\\dA-Z]{4}\\d{20}",BE:"\\d{12}",BH:"[A-Z]{4}[\\dA-Z]{14}",BA:"\\d{16}",BR:"\\d{23}[A-Z][\\dA-Z]",BG:"[A-Z]{4}\\d{6}[\\dA-Z]{8}",CR:"\\d{17}",HR:"\\d{17}",CY:"\\d{8}[\\dA-Z]{16}",CZ:"\\d{20}",DK:"\\d{14}",DO:"[A-Z]{4}\\d{20}",EE:"\\d{16}",FO:"\\d{14}",FI:"\\d{14}",FR:"\\d{10}[\\dA-Z]{11}\\d{2}",GE:"[\\dA-Z]{2}\\d{16}",DE:"\\d{18}",GI:"[A-Z]{4}[\\dA-Z]{15}",GR:"\\d{7}[\\dA-Z]{16}",GL:"\\d{14}",GT:"[\\dA-Z]{4}[\\dA-Z]{20}",HU:"\\d{24}",IS:"\\d{22}",IE:"[\\dA-Z]{4}\\d{14}",IL:"\\d{19}",IT:"[A-Z]\\d{10}[\\dA-Z]{12}",KZ:"\\d{3}[\\dA-Z]{13}",KW:"[A-Z]{4}[\\dA-Z]{22}",LV:"[A-Z]{4}[\\dA-Z]{13}",LB:"\\d{4}[\\dA-Z]{20}",LI:"\\d{5}[\\dA-Z]{12}",LT:"\\d{16}",LU:"\\d{3}[\\dA-Z]{13}",MK:"\\d{3}[\\dA-Z]{10}\\d{2}",MT:"[A-Z]{4}\\d{5}[\\dA-Z]{18}",MR:"\\d{23}",MU:"[A-Z]{4}\\d{19}[A-Z]{3}",MC:"\\d{10}[\\dA-Z]{11}\\d{2}",MD:"[\\dA-Z]{2}\\d{18}",ME:"\\d{18}",NL:"[A-Z]{4}\\d{10}",NO:"\\d{11}",PK:"[\\dA-Z]{4}\\d{16}",PS:"[\\dA-Z]{4}\\d{21}",PL:"\\d{24}",PT:"\\d{21}",RO:"[A-Z]{4}[\\dA-Z]{16}",SM:"[A-Z]\\d{10}[\\dA-Z]{12}",SA:"\\d{2}[\\dA-Z]{18}",RS:"\\d{18}",SK:"\\d{20}",SI:"\\d{15}",ES:"\\d{20}",SE:"\\d{20}",CH:"\\d{5}[\\dA-Z]{12}",TN:"\\d{20}",TR:"\\d{5}[\\dA-Z]{17}",AE:"\\d{3}\\d{16}",GB:"[A-Z]{4}\\d{14}",VG:"[\\dA-Z]{4}\\d{16}"},g=h[c],"undefined"!=typeof g&&(i=new RegExp("^[A-Z]{2}\\d{2}"+g+"$",""),!i.test(l)))return!1;for(d=l.substring(4,l.length)+l.substring(0,4),j=0;j9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/)},"Please specify a valid mobile number"),a.validator.addMethod("nieES",function(a){"use strict";return a=a.toUpperCase(),a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")?/^[T]{1}/.test(a)?a[8]===/^[T]{1}[A-Z0-9]{8}$/.test(a):/^[XYZ]{1}/.test(a)?a[8]==="TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.replace("X","0").replace("Y","1").replace("Z","2").substring(0,8)%23):!1:!1},"Please specify a valid NIE number."),a.validator.addMethod("nifES",function(a){"use strict";return a=a.toUpperCase(),a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")?/^[0-9]{8}[A-Z]{1}$/.test(a)?"TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,0)%23)===a.charAt(8):/^[KLM]{1}/.test(a)?a[8]===String.fromCharCode(64):!1:!1},"Please specify a valid NIF number."),a.validator.addMethod("nowhitespace",function(a,b){return this.optional(b)||/^\S+$/i.test(a)},"No white space please"),a.validator.addMethod("pattern",function(a,b,c){return this.optional(b)?!0:("string"==typeof c&&(c=new RegExp("^(?:"+c+")$")),c.test(a))},"Invalid format."),a.validator.addMethod("phoneNL",function(a,b){return this.optional(b)||/^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test(a)},"Please specify a valid phone number."),a.validator.addMethod("phoneUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/)},"Please specify a valid phone number"),a.validator.addMethod("phoneUS",function(a,b){return a=a.replace(/\s+/g,""),this.optional(b)||a.length>9&&a.match(/^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/)},"Please specify a valid phone number"),a.validator.addMethod("phonesUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/)},"Please specify a valid uk phone number"),a.validator.addMethod("postalCodeCA",function(a,b){return this.optional(b)||/^[ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeBR",function(a,b){return this.optional(b)||/^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test(a)},"Informe um CEP válido."),a.validator.addMethod("postalcodeIT",function(a,b){return this.optional(b)||/^\d{5}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeNL",function(a,b){return this.optional(b)||/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postcodeUK",function(a,b){return this.optional(b)||/^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test(a)},"Please specify a valid UK postcode"),a.validator.addMethod("require_from_group",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_req_grp")?f.data("valid_req_grp"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length>=d[0];return f.data("valid_req_grp",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),h},a.validator.format("Please fill at least {0} of these fields.")),a.validator.addMethod("skip_or_fill_minimum",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_skip")?f.data("valid_skip"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length,i=0===h||h>=d[0];return f.data("valid_skip",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),i},a.validator.format("Please either skip these fields or fill at least {0} of them.")),jQuery.validator.addMethod("stateUS",function(a,b,c){var d,e="undefined"==typeof c,f=e||"undefined"==typeof c.caseSensitive?!1:c.caseSensitive,g=e||"undefined"==typeof c.includeTerritories?!1:c.includeTerritories,h=e||"undefined"==typeof c.includeMilitary?!1:c.includeMilitary;return d=g||h?g&&h?"^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":g?"^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":"^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$":"^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$",d=f?new RegExp(d):new RegExp(d,"i"),this.optional(b)||d.test(a)},"Please specify a valid state"),a.validator.addMethod("strippedminlength",function(b,c,d){return a(b).text().length>=d},a.validator.format("Please enter at least {0} characters")),a.validator.addMethod("time",function(a,b){return this.optional(b)||/^([01]\d|2[0-3])(:[0-5]\d){1,2}$/.test(a)},"Please enter a valid time, between 00:00 and 23:59"),a.validator.addMethod("time12h",function(a,b){return this.optional(b)||/^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(a)},"Please enter a valid time in 12-hour am/pm format"),a.validator.addMethod("url2",function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},a.validator.messages.url),a.validator.addMethod("vinUS",function(a){if(17!==a.length)return!1;var b,c,d,e,f,g,h=["A","B","C","D","E","F","G","H","J","K","L","M","N","P","R","S","T","U","V","W","X","Y","Z"],i=[1,2,3,4,5,6,7,8,1,2,3,4,5,7,9,2,3,4,5,6,7,8,9],j=[8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2],k=0;for(b=0;17>b;b++){if(e=j[b],d=a.slice(b,b+1),8===b&&(g=d),isNaN(d)){for(c=0;c