From c53d8d026021f97075fb2f4940ba22793c38fb6e Mon Sep 17 00:00:00 2001 From: davehauenstein Date: Wed, 15 Apr 2009 22:06:42 +0000 Subject: added toolbar; functionality includes refresh button to get back to original page, print article, email a link to the article with a personal note git-svn-id: http://arc90labs-readability.googlecode.com/svn/trunk@31 d4e419ec-0920-11de-bbfd-a7c1bc4c261e --- close.html | 10 + css/readability-print.css | 4 +- css/readability.css | 75 +- email.php | 427 ++++++++++ images/email-head.gif | Bin 0 -> 884 bytes images/email-readability.gif | Bin 0 -> 561 bytes images/read-email.png | Bin 0 -> 734 bytes images/read-print.png | Bin 0 -> 741 bytes images/read-refresh.png | Bin 0 -> 874 bytes images/twitter.png | Bin 0 -> 510 bytes js/readability.js | 56 +- lib/Zend/Exception.php | 30 + lib/Zend/Filter.php | 115 +++ lib/Zend/Filter/Alnum.php | 115 +++ lib/Zend/Filter/Alpha.php | 115 +++ lib/Zend/Filter/BaseName.php | 50 ++ lib/Zend/Filter/Digits.php | 82 ++ lib/Zend/Filter/Dir.php | 50 ++ lib/Zend/Filter/Exception.php | 37 + lib/Zend/Filter/File/LowerCase.php | 84 ++ lib/Zend/Filter/File/Rename.php | 282 +++++++ lib/Zend/Filter/File/UpperCase.php | 84 ++ lib/Zend/Filter/HtmlEntities.php | 122 +++ lib/Zend/Filter/Inflector.php | 497 +++++++++++ lib/Zend/Filter/Input.php | 926 +++++++++++++++++++++ lib/Zend/Filter/Int.php | 50 ++ lib/Zend/Filter/Interface.php | 40 + lib/Zend/Filter/PregReplace.php | 156 ++++ lib/Zend/Filter/RealPath.php | 50 ++ lib/Zend/Filter/StringToLower.php | 76 ++ lib/Zend/Filter/StringToUpper.php | 76 ++ lib/Zend/Filter/StringTrim.php | 97 +++ lib/Zend/Filter/StripNewlines.php | 48 ++ lib/Zend/Filter/StripTags.php | 294 +++++++ lib/Zend/Filter/Word/CamelCaseToDash.php | 44 + lib/Zend/Filter/Word/CamelCaseToSeparator.php | 49 ++ lib/Zend/Filter/Word/CamelCaseToUnderscore.php | 44 + lib/Zend/Filter/Word/DashToCamelCase.php | 44 + lib/Zend/Filter/Word/DashToSeparator.php | 42 + lib/Zend/Filter/Word/DashToUnderscore.php | 45 + lib/Zend/Filter/Word/Separator/Abstract.php | 76 ++ lib/Zend/Filter/Word/SeparatorToCamelCase.php | 52 ++ lib/Zend/Filter/Word/SeparatorToDash.php | 46 ++ lib/Zend/Filter/Word/SeparatorToSeparator.php | 129 +++ lib/Zend/Filter/Word/UnderscoreToCamelCase.php | 44 + lib/Zend/Filter/Word/UnderscoreToDash.php | 45 + lib/Zend/Filter/Word/UnderscoreToSeparator.php | 45 + lib/Zend/Loader.php | 263 ++++++ lib/Zend/Loader/Exception.php | 35 + lib/Zend/Loader/PluginLoader.php | 464 +++++++++++ lib/Zend/Loader/PluginLoader/Exception.php | 38 + lib/Zend/Loader/PluginLoader/Interface.php | 74 ++ lib/Zend/Mail.php | 1042 +++++++++++++++++++++++ lib/Zend/Mail/Exception.php | 37 + lib/Zend/Mail/Message.php | 112 +++ lib/Zend/Mail/Message/File.php | 96 +++ lib/Zend/Mail/Message/Interface.php | 55 ++ lib/Zend/Mail/Part.php | 489 +++++++++++ lib/Zend/Mail/Part/File.php | 198 +++++ lib/Zend/Mail/Part/Interface.php | 136 +++ lib/Zend/Mail/Protocol/Abstract.php | 385 +++++++++ lib/Zend/Mail/Protocol/Exception.php | 39 + lib/Zend/Mail/Protocol/Imap.php | 837 +++++++++++++++++++ lib/Zend/Mail/Protocol/Pop3.php | 471 +++++++++++ lib/Zend/Mail/Protocol/Smtp.php | 443 ++++++++++ lib/Zend/Mail/Protocol/Smtp/Auth/Crammd5.php | 108 +++ lib/Zend/Mail/Protocol/Smtp/Auth/Login.php | 98 +++ lib/Zend/Mail/Protocol/Smtp/Auth/Plain.php | 96 +++ lib/Zend/Mail/Storage.php | 39 + lib/Zend/Mail/Storage/Abstract.php | 366 +++++++++ lib/Zend/Mail/Storage/Exception.php | 39 + lib/Zend/Mail/Storage/Folder.php | 236 ++++++ lib/Zend/Mail/Storage/Folder/Interface.php | 60 ++ lib/Zend/Mail/Storage/Folder/Maildir.php | 265 ++++++ lib/Zend/Mail/Storage/Folder/Mbox.php | 264 ++++++ lib/Zend/Mail/Storage/Imap.php | 644 +++++++++++++++ lib/Zend/Mail/Storage/Maildir.php | 475 +++++++++++ lib/Zend/Mail/Storage/Mbox.php | 447 ++++++++++ lib/Zend/Mail/Storage/Pop3.php | 328 ++++++++ lib/Zend/Mail/Storage/Writable/Interface.php | 108 +++ lib/Zend/Mail/Storage/Writable/Maildir.php | 1049 ++++++++++++++++++++++++ lib/Zend/Mail/Transport/Abstract.php | 350 ++++++++ lib/Zend/Mail/Transport/Exception.php | 39 + lib/Zend/Mail/Transport/Sendmail.php | 170 ++++ lib/Zend/Mail/Transport/Smtp.php | 241 ++++++ lib/Zend/Mime.php | 365 +++++++++ lib/Zend/Mime/Decode.php | 243 ++++++ lib/Zend/Mime/Exception.php | 36 + lib/Zend/Mime/Message.php | 285 +++++++ lib/Zend/Mime/Part.php | 216 +++++ lib/Zend/Registry.php | 207 +++++ lib/Zend/Validate.php | 171 ++++ lib/Zend/Validate/Abstract.php | 358 ++++++++ lib/Zend/Validate/Alnum.php | 120 +++ lib/Zend/Validate/Alpha.php | 120 +++ lib/Zend/Validate/Barcode.php | 99 +++ lib/Zend/Validate/Barcode/Ean13.php | 112 +++ lib/Zend/Validate/Barcode/UpcA.php | 100 +++ lib/Zend/Validate/Between.php | 200 +++++ lib/Zend/Validate/Ccnum.php | 111 +++ lib/Zend/Validate/Date.php | 235 ++++++ lib/Zend/Validate/Digits.php | 100 +++ lib/Zend/Validate/EmailAddress.php | 247 ++++++ lib/Zend/Validate/Exception.php | 37 + lib/Zend/Validate/File/Count.php | 275 +++++++ lib/Zend/Validate/File/Crc32.php | 179 ++++ lib/Zend/Validate/File/ExcludeExtension.php | 94 +++ lib/Zend/Validate/File/ExcludeMimeType.php | 91 ++ lib/Zend/Validate/File/Exists.php | 203 +++++ lib/Zend/Validate/File/Extension.php | 234 ++++++ lib/Zend/Validate/File/FilesSize.php | 163 ++++ lib/Zend/Validate/File/Hash.php | 195 +++++ lib/Zend/Validate/File/ImageSize.php | 370 +++++++++ lib/Zend/Validate/File/IsCompressed.php | 133 +++ lib/Zend/Validate/File/IsImage.php | 137 ++++ lib/Zend/Validate/File/Md5.php | 183 +++++ lib/Zend/Validate/File/MimeType.php | 283 +++++++ lib/Zend/Validate/File/NotExists.php | 84 ++ lib/Zend/Validate/File/Sha1.php | 181 ++++ lib/Zend/Validate/File/Size.php | 404 +++++++++ lib/Zend/Validate/File/Upload.php | 234 ++++++ lib/Zend/Validate/Float.php | 75 ++ lib/Zend/Validate/GreaterThan.php | 114 +++ lib/Zend/Validate/Hex.php | 74 ++ lib/Zend/Validate/Hostname.php | 444 ++++++++++ lib/Zend/Validate/Hostname/At.php | 50 ++ lib/Zend/Validate/Hostname/Ch.php | 50 ++ lib/Zend/Validate/Hostname/De.php | 58 ++ lib/Zend/Validate/Hostname/Fi.php | 50 ++ lib/Zend/Validate/Hostname/Hu.php | 50 ++ lib/Zend/Validate/Hostname/Interface.php | 52 ++ lib/Zend/Validate/Hostname/Li.php | 50 ++ lib/Zend/Validate/Hostname/No.php | 52 ++ lib/Zend/Validate/Hostname/Se.php | 50 ++ lib/Zend/Validate/Identical.php | 117 +++ lib/Zend/Validate/InArray.php | 138 ++++ lib/Zend/Validate/Int.php | 73 ++ lib/Zend/Validate/Interface.php | 71 ++ lib/Zend/Validate/Ip.php | 71 ++ lib/Zend/Validate/LessThan.php | 113 +++ lib/Zend/Validate/NotEmpty.php | 70 ++ lib/Zend/Validate/Regex.php | 125 +++ lib/Zend/Validate/StringLength.php | 223 +++++ log.txt | 0 144 files changed, 24331 insertions(+), 33 deletions(-) create mode 100644 close.html create mode 100644 email.php create mode 100644 images/email-head.gif create mode 100644 images/email-readability.gif create mode 100644 images/read-email.png create mode 100644 images/read-print.png create mode 100644 images/read-refresh.png create mode 100644 images/twitter.png create mode 100644 lib/Zend/Exception.php create mode 100644 lib/Zend/Filter.php create mode 100644 lib/Zend/Filter/Alnum.php create mode 100644 lib/Zend/Filter/Alpha.php create mode 100644 lib/Zend/Filter/BaseName.php create mode 100644 lib/Zend/Filter/Digits.php create mode 100644 lib/Zend/Filter/Dir.php create mode 100644 lib/Zend/Filter/Exception.php create mode 100644 lib/Zend/Filter/File/LowerCase.php create mode 100644 lib/Zend/Filter/File/Rename.php create mode 100644 lib/Zend/Filter/File/UpperCase.php create mode 100644 lib/Zend/Filter/HtmlEntities.php create mode 100644 lib/Zend/Filter/Inflector.php create mode 100644 lib/Zend/Filter/Input.php create mode 100644 lib/Zend/Filter/Int.php create mode 100644 lib/Zend/Filter/Interface.php create mode 100644 lib/Zend/Filter/PregReplace.php create mode 100644 lib/Zend/Filter/RealPath.php create mode 100644 lib/Zend/Filter/StringToLower.php create mode 100644 lib/Zend/Filter/StringToUpper.php create mode 100644 lib/Zend/Filter/StringTrim.php create mode 100644 lib/Zend/Filter/StripNewlines.php create mode 100644 lib/Zend/Filter/StripTags.php create mode 100644 lib/Zend/Filter/Word/CamelCaseToDash.php create mode 100644 lib/Zend/Filter/Word/CamelCaseToSeparator.php create mode 100644 lib/Zend/Filter/Word/CamelCaseToUnderscore.php create mode 100644 lib/Zend/Filter/Word/DashToCamelCase.php create mode 100644 lib/Zend/Filter/Word/DashToSeparator.php create mode 100644 lib/Zend/Filter/Word/DashToUnderscore.php create mode 100644 lib/Zend/Filter/Word/Separator/Abstract.php create mode 100644 lib/Zend/Filter/Word/SeparatorToCamelCase.php create mode 100644 lib/Zend/Filter/Word/SeparatorToDash.php create mode 100644 lib/Zend/Filter/Word/SeparatorToSeparator.php create mode 100644 lib/Zend/Filter/Word/UnderscoreToCamelCase.php create mode 100644 lib/Zend/Filter/Word/UnderscoreToDash.php create mode 100644 lib/Zend/Filter/Word/UnderscoreToSeparator.php create mode 100644 lib/Zend/Loader.php create mode 100644 lib/Zend/Loader/Exception.php create mode 100644 lib/Zend/Loader/PluginLoader.php create mode 100644 lib/Zend/Loader/PluginLoader/Exception.php create mode 100644 lib/Zend/Loader/PluginLoader/Interface.php create mode 100644 lib/Zend/Mail.php create mode 100644 lib/Zend/Mail/Exception.php create mode 100644 lib/Zend/Mail/Message.php create mode 100644 lib/Zend/Mail/Message/File.php create mode 100644 lib/Zend/Mail/Message/Interface.php create mode 100644 lib/Zend/Mail/Part.php create mode 100644 lib/Zend/Mail/Part/File.php create mode 100644 lib/Zend/Mail/Part/Interface.php create mode 100644 lib/Zend/Mail/Protocol/Abstract.php create mode 100644 lib/Zend/Mail/Protocol/Exception.php create mode 100644 lib/Zend/Mail/Protocol/Imap.php create mode 100644 lib/Zend/Mail/Protocol/Pop3.php create mode 100644 lib/Zend/Mail/Protocol/Smtp.php create mode 100644 lib/Zend/Mail/Protocol/Smtp/Auth/Crammd5.php create mode 100644 lib/Zend/Mail/Protocol/Smtp/Auth/Login.php create mode 100644 lib/Zend/Mail/Protocol/Smtp/Auth/Plain.php create mode 100644 lib/Zend/Mail/Storage.php create mode 100644 lib/Zend/Mail/Storage/Abstract.php create mode 100644 lib/Zend/Mail/Storage/Exception.php create mode 100644 lib/Zend/Mail/Storage/Folder.php create mode 100644 lib/Zend/Mail/Storage/Folder/Interface.php create mode 100644 lib/Zend/Mail/Storage/Folder/Maildir.php create mode 100644 lib/Zend/Mail/Storage/Folder/Mbox.php create mode 100644 lib/Zend/Mail/Storage/Imap.php create mode 100644 lib/Zend/Mail/Storage/Maildir.php create mode 100644 lib/Zend/Mail/Storage/Mbox.php create mode 100644 lib/Zend/Mail/Storage/Pop3.php create mode 100644 lib/Zend/Mail/Storage/Writable/Interface.php create mode 100644 lib/Zend/Mail/Storage/Writable/Maildir.php create mode 100644 lib/Zend/Mail/Transport/Abstract.php create mode 100644 lib/Zend/Mail/Transport/Exception.php create mode 100644 lib/Zend/Mail/Transport/Sendmail.php create mode 100644 lib/Zend/Mail/Transport/Smtp.php create mode 100644 lib/Zend/Mime.php create mode 100644 lib/Zend/Mime/Decode.php create mode 100644 lib/Zend/Mime/Exception.php create mode 100644 lib/Zend/Mime/Message.php create mode 100644 lib/Zend/Mime/Part.php create mode 100644 lib/Zend/Registry.php create mode 100644 lib/Zend/Validate.php create mode 100644 lib/Zend/Validate/Abstract.php create mode 100644 lib/Zend/Validate/Alnum.php create mode 100644 lib/Zend/Validate/Alpha.php create mode 100644 lib/Zend/Validate/Barcode.php create mode 100644 lib/Zend/Validate/Barcode/Ean13.php create mode 100644 lib/Zend/Validate/Barcode/UpcA.php create mode 100644 lib/Zend/Validate/Between.php create mode 100644 lib/Zend/Validate/Ccnum.php create mode 100644 lib/Zend/Validate/Date.php create mode 100644 lib/Zend/Validate/Digits.php create mode 100644 lib/Zend/Validate/EmailAddress.php create mode 100644 lib/Zend/Validate/Exception.php create mode 100644 lib/Zend/Validate/File/Count.php create mode 100644 lib/Zend/Validate/File/Crc32.php create mode 100644 lib/Zend/Validate/File/ExcludeExtension.php create mode 100644 lib/Zend/Validate/File/ExcludeMimeType.php create mode 100644 lib/Zend/Validate/File/Exists.php create mode 100644 lib/Zend/Validate/File/Extension.php create mode 100644 lib/Zend/Validate/File/FilesSize.php create mode 100644 lib/Zend/Validate/File/Hash.php create mode 100644 lib/Zend/Validate/File/ImageSize.php create mode 100644 lib/Zend/Validate/File/IsCompressed.php create mode 100644 lib/Zend/Validate/File/IsImage.php create mode 100644 lib/Zend/Validate/File/Md5.php create mode 100644 lib/Zend/Validate/File/MimeType.php create mode 100644 lib/Zend/Validate/File/NotExists.php create mode 100644 lib/Zend/Validate/File/Sha1.php create mode 100644 lib/Zend/Validate/File/Size.php create mode 100644 lib/Zend/Validate/File/Upload.php create mode 100644 lib/Zend/Validate/Float.php create mode 100644 lib/Zend/Validate/GreaterThan.php create mode 100644 lib/Zend/Validate/Hex.php create mode 100644 lib/Zend/Validate/Hostname.php create mode 100644 lib/Zend/Validate/Hostname/At.php create mode 100644 lib/Zend/Validate/Hostname/Ch.php create mode 100644 lib/Zend/Validate/Hostname/De.php create mode 100644 lib/Zend/Validate/Hostname/Fi.php create mode 100644 lib/Zend/Validate/Hostname/Hu.php create mode 100644 lib/Zend/Validate/Hostname/Interface.php create mode 100644 lib/Zend/Validate/Hostname/Li.php create mode 100644 lib/Zend/Validate/Hostname/No.php create mode 100644 lib/Zend/Validate/Hostname/Se.php create mode 100644 lib/Zend/Validate/Identical.php create mode 100644 lib/Zend/Validate/InArray.php create mode 100644 lib/Zend/Validate/Int.php create mode 100644 lib/Zend/Validate/Interface.php create mode 100644 lib/Zend/Validate/Ip.php create mode 100644 lib/Zend/Validate/LessThan.php create mode 100644 lib/Zend/Validate/NotEmpty.php create mode 100644 lib/Zend/Validate/Regex.php create mode 100644 lib/Zend/Validate/StringLength.php create mode 100644 log.txt diff --git a/close.html b/close.html new file mode 100644 index 0000000..dca0447 --- /dev/null +++ b/close.html @@ -0,0 +1,10 @@ + + + + + Closing + + +
+ + \ No newline at end of file diff --git a/css/readability-print.css b/css/readability-print.css index 83aa757..a4c8cb3 100755 --- a/css/readability-print.css +++ b/css/readability-print.css @@ -1,5 +1,7 @@ - #readInner { width: 100% !important; font-size: 12pt; +} +#readTools { + display: none; } \ No newline at end of file diff --git a/css/readability.css b/css/readability.css index 15292d0..f7e8f41 100755 --- a/css/readability.css +++ b/css/readability.css @@ -3,38 +3,31 @@ /* #readOverlay { - background-image: none; - - - background: #eee; + background-image: none; + background: #eee; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; } */ - #readOverlay { display: block; position: absolute; top: 0; left: 0; - z-index: 250000; width: 100%; } - #readInner { text-align: left; - line-height: 1.4em; + line-height: 1.4em; margin: 1em auto; max-width: 800px; - } #readInner a { color: blue; - text-decoration: underline; + text-decoration: underline; } - #readInner * { - margin-bottom: 16px; + margin-bottom: 16px; border: none; background: none; } @@ -43,20 +36,19 @@ margin-right: 12px; margin-bottom: 12px; } - #readInner h1 { display: block; width: 100%; border-bottom: 1px solid #333; font-size: 1.2em; } -#readInner blockquote { +#readInner blockquote { margin-left: 3em; margin-right: 3em; } #readFooter { display: block; - border-top: 1px solid #333; + border-top: 1px solid #333; text-align: center; } div.footer-right { @@ -67,17 +59,36 @@ div.footer-right { margin-top: 18px } span.version { - display: none; + display: none; } +/* Article Tools */ +#readTools { + width: 34px; + height: 150px; + position: fixed; + z-index: 100; + top: 10px; + left: 10px; +} +#readTools a { + margin-bottom: 8px; + display: block; +} +#readTools a img { + border: none; +} /* ---------------- USER-CONFIGURABLE STYLING --------------------- */ + /* ---------------- USER-CONFIGURABLE STYLING --------------------- */ + /* Size options */ + .size-small { font-size: 12px; } .size-medium { - font-size: 18px; + font-size: 18px; } .size-large { font-size: 26px; @@ -85,14 +96,13 @@ span.version { .size-x-large { font-size: 34px; } - /* Style options */ + .style-novel { font-family:"Palatino Linotype", "Book Antiqua", Palatino, serif; background: #F4F3DB; color: #222; } - .style-ebook { font-family:Arial, Helvetica, sans-serif; background: #eee; @@ -102,24 +112,22 @@ span.version { font-family: "Arial Black", Gadget, sans-serif; font-weight: normal; } - .style-newspaper { font-family:"Times New Roman", Times, serif; - background: #F7F7F7; + background: #FFF; color: #222; } .style-newspaper h1 { text-transform:capitalize; font-family: Georgia, "Times New Roman", Times, serif; } - .style-terminal { font-family: "Lucida Console", Monaco, monospace; background: #1D4E2C; color: #C6FFC6; } - /* Margin Options */ + .margin-x-wide { width: 35%; } @@ -135,6 +143,7 @@ span.version { /* ---------------- USER-CONFIGURABLE STYLING --------------------- */ /* ------ DEBUG ------- */ + .bug-green { background: #BBF9B0; border: 4px solid green; @@ -143,8 +152,24 @@ span.version { background: red; } .bug-yellow { - background: #FFFF8E; + background: #FFFF8E; } .bug-blue { - background: #BFDFFF; + background: #BFDFFF; } + +/* ---------------- EMAIL POP UP --------------------- */ + +#email-container{ + position: fixed; + top: 60px; + left: 50%; + margin: 0 0 0 -240px; + padding: 0; + width: 480px; + height: 460px; + border: solid 3px #666; + background-color: #fff; + z-index: 100 !important; + overflow: hidden; +} \ No newline at end of file diff --git a/email.php b/email.php new file mode 100644 index 0000000..a83b97a --- /dev/null +++ b/email.php @@ -0,0 +1,427 @@ +addFilter(new Zend_Filter_StripTags()) + ->addFilter(new Zend_Filter_StringTrim()); + + $pageTitle = array_key_exists('pageTitle', $_GET) ? $filters->filter($_GET['pageTitle']) : ''; + $pageUrl = array_key_exists('pageUrl', $_GET) ? $filters->filter($_GET['pageUrl']) : ''; + $page = 'form'; + $errors = array(); + + if('post' == strtolower($_SERVER['REQUEST_METHOD'])) + { + // someone sent over an invalid + if(!Readability::hasValidParams()) + { + Readability::logMessage("ERROR:Someone tried to send a request with an invalid set of parameters."); + die(); + } + + require_once 'Zend/Validate/EmailAddress.php'; + + //FILTER DATA + + $from = $filters->filter($_POST['from']); + $to = $filters->filter($_POST['to']); + $to = array_map('trim', split(',', $to)); + $note = $filters->filter($_POST['note']); + $key = $filters->filter($_POST['key']); + + if(!Readability::validateSecureKey($key)) + { + $errors[] = 'key'; + Readability::logMessage("ERROR:Someone tried to send an email with an invalid key."); + } + + // VALIDATE DATA + + $emailValidator = new Zend_Validate_EmailAddress(); + + if(!$emailValidator->isValid($_POST['from'])) + { + $errors[] = 'from'; + } + + if(count($to) == 0) + { + $errors[] = 'to'; + } + else + { + foreach($to as $toAddress) + { + if(!$emailValidator->isValid($toAddress)) + { + $errors[] = 'to'; + break; + } + } + } + + // NO ERRORS SEND EMAIL + if(count($errors) == 0) + { + // store the from address so it's saved for future use + setcookie("from", $from, time()+3600*24*7*4, "/"); + + require_once 'Zend/Mail.php'; + require_once 'Zend/Mail/Transport/Smtp.php'; + + $mailer = new Zend_Mail_Transport_Smtp('smtp.googlemail.com', Array( + 'auth' => 'login', + 'username' => 'readability@arc90.com', + 'password' => 'arc90inc', + 'ssl' => 'ssl', + 'port' => 465, + )); + $mailer->EOL = "\r\n"; // gmail is fussy about this + Zend_Mail::setDefaultTransport($mailer); + + $body = ''; + $body = ''; + $body = ''; + $body .= '
'; + $body .= '

This page was sent to you by: '.$from.'

'; + $body .= '

Message from sender:

'.stripslashes($note).'

'; + $body .= '

Just click this link: '.$pageTitle.'

'; + $body .= '
'; + $body .= '

Sent from Readability | An Arc90 lab experiment

'; + $body .= '

'; + $body .= ''; + + $mail = new Zend_Mail(); + $mail->setBodyHtml($body); + $mail->setFrom($from); + + foreach($to as $toAddress) + { + $mail->addTo($toAddress); + } + + $mail->setSubject("Arc90 Readability: {$pageTitle}"); + + try + { + if(!$mail->send()) + { + Readability::logMessage("ERROR:There was an error sending the email. [to:".implode(', ', $to).", from:{$from}, notes:{$note}, pageUrl: {$pageUrl}, pageTitle: {$pageTitle}]"); + } + else + { + $page = 'complete'; + } + } + catch(Exception $e) + { + Readability::logMessage("ERROR:There was an exception sending the email. [to:".implode(', ', $to).", from:{$from}, notes:{$note}, pageUrl: {$pageUrl}, pageTitle: {$pageTitle}]"); + Readability::logMessage("ERROR:".$e->getMessage()); + } + + //header('location: close.html'); + } + } // end of: if method == POST + + elseif('get' == strtolower($_SERVER['REQUEST_METHOD'])) + { + $_SESSION['secureKey'] = Readability::generateSecureKey(); + } + + class Readability + { + public static function isError($field, $errors) + { + if(in_array($field, $errors)) + { + return TRUE; + } + return FALSE; + } + + public static function getErrorClass($field, $errors) + { + if(in_array($field, $errors)) + { + return 'class = "error"'; + } + return ''; + } + + public static function getParam($param) + { + if(isset($_POST) && array_key_exists($param, $_POST)) + { + return $_POST[$param]; + } + elseif(isset($_COOKIE) && array_key_exists($param, $_COOKIE)) + { + return $_COOKIE[$param]; + } + return ''; + } + + public static function logMessage($message) + { + $logFile = dirname(__FILE__) . '/log.txt'; + + $handle = @fopen($logFile, 'a'); + if(is_resource($handle)) + { + $message = date('Y-m-d G:i:s') . ' :: ' . $message . "\n"; + fwrite($handle, $message); + fclose($handle); + } + } + + public static function generateSecureKey($length = 8) + { + $sucureKey = ""; + $possible = "012*3456)789b(cdfg#hjkmn@pqrs!tvwx[yz"; + + for($x=0; $x < $length; $x++) + { + $char = substr($possible, mt_rand(0, strlen($possible)-1), 1); + + if (!strstr($sucureKey, $char)) + { + $sucureKey .= $char; + } + } + return $sucureKey; + } + + /** + * this adds a small (very small) level of security + * + * @param string $testKey + * @return void + * @author David Hauenstein + */ + public static function validateSecureKey($testKey) + { + if(!array_key_exists('secureKey', $_SESSION)) + { + $_SESSION['secureKey'] = self::generateSecureKey(); + return false; + } + else + { + if($testKey != $_SESSION['secureKey']) + { + return false; + } + } + return true; + } + + public static function emailAsLinks($addresses) + { + $toReturn = ''; + foreach($addresses as $address) + { + $toReturn .= '' . $address . ', '; + } + return substr($toReturn, 0, strlen($toReturn)-2); + } + + public static function hasValidParams() + { + $requiredParams = array('from', 'to', 'note', 'key'); + $sentParams = array_keys($_POST); + foreach($requiredParams as $required) + { + if(!in_array($required, $sentParams)) + { + return false; + } + } + return true; + } + } +?> +xml version="1.0" encoding="utf-8" ?> + + + + Readability + + + + +
+

Email Page

+ + +
+
+ + /> + +

+ This field should be a valid email address. +

+ +
+
+ + /> + +

+ Please ensure that all addresses are valid email adderesses. +

+ +

+ Seperate multiple recipients with commas. +

+
+
+ +

+ +

+
+
+ + +
+
+ + +
+ + + + +
+ +
+

+ Thanks for using Readability! +

+

+ A link to this page has been sent to +

+
+ +
+ + \ No newline at end of file diff --git a/images/email-head.gif b/images/email-head.gif new file mode 100644 index 0000000..1f08afc Binary files /dev/null and b/images/email-head.gif differ diff --git a/images/email-readability.gif b/images/email-readability.gif new file mode 100644 index 0000000..f8465ab Binary files /dev/null and b/images/email-readability.gif differ diff --git a/images/read-email.png b/images/read-email.png new file mode 100644 index 0000000..0bc865b Binary files /dev/null and b/images/read-email.png differ diff --git a/images/read-print.png b/images/read-print.png new file mode 100644 index 0000000..8790be3 Binary files /dev/null and b/images/read-print.png differ diff --git a/images/read-refresh.png b/images/read-refresh.png new file mode 100644 index 0000000..320d3f5 Binary files /dev/null and b/images/read-refresh.png differ diff --git a/images/twitter.png b/images/twitter.png new file mode 100644 index 0000000..06a8485 Binary files /dev/null and b/images/twitter.png differ diff --git a/js/readability.js b/js/readability.js index 934ffb8..97cd2bb 100755 --- a/js/readability.js +++ b/js/readability.js @@ -1,9 +1,12 @@ -var readabilityVersion = "0.3"; +var readabilityVersion = "0.4"; +var emailSrc = 'http://lab.arc90.com/experiments/readability/email.php'; +var iframeLoads = 0; (function(){ var objOverlay = document.createElement("div"); var objinnerDiv = document.createElement("div"); - + var articleTools = document.createElement("DIV"); + objOverlay.id = "readOverlay"; objinnerDiv.id = "readInner"; @@ -12,7 +15,20 @@ var readabilityVersion = "0.3"; objOverlay.className = readStyle; objinnerDiv.className = readMargin + " " + readSize; + // Set up tools widget + + // NOTE THE IMAGE URL'S HERE !!!!!!!!!!!!!!!!! + // NOTE THE IMAGE URL'S HERE !!!!!!!!!!!!!!!!! + // NOTE THE IMAGE URL'S HERE !!!!!!!!!!!!!!!!! + articleTools.id = "readTools"; + articleTools.innerHTML = "\ + \ + \ + \ + "; + objinnerDiv.appendChild(grabArticle()); // Get the article and place it inside the inner Div + objOverlay.appendChild(articleTools); objOverlay.appendChild(objinnerDiv); // Insert the inner div into the overlay // For totally hosed HTML, add body node that can't be found because of bad HTML or something. @@ -113,25 +129,24 @@ function grabArticle() { topDiv = clean(topDiv, "h2"); topDiv = clean(topDiv, "iframe"); + // Add the footer and contents: articleFooter.id = "readFooter"; articleFooter.innerHTML = "\ \ \ "; - + articleContent.appendChild(topDiv); articleContent.appendChild(articleFooter); - + return articleContent; } // Get the inner text of a node - cross browser compatibly. -function getInnerText(e) -{ +function getInnerText(e) { if (navigator.appName == "Microsoft Internet Explorer") return e.innerText; else @@ -210,3 +225,30 @@ function clean(e, tags, minWords) { return e; } +function emailBox() { + var emailContainer = document.getElementById('email-container'); + if(null != emailContainer) + { + return; + } + + var emailContainer = document.createElement('div'); + emailContainer.setAttribute('id', 'email-container'); + emailContainer.innerHTML = ''; + + document.body.appendChild(emailContainer); +} + +function removeFrame() +{ + ++iframeLoads; + if(iframeLoads >= 6) + { + var emailContainer = document.getElementById('email-container'); + if(null != emailContainer) { + emailContainer.parentNode.removeChild(emailContainer); + } + // reset the count + iframeLoads = 0; + } +} \ No newline at end of file diff --git a/lib/Zend/Exception.php b/lib/Zend/Exception.php new file mode 100644 index 0000000..599d8a0 --- /dev/null +++ b/lib/Zend/Exception.php @@ -0,0 +1,30 @@ +_filters[] = $filter; + return $this; + } + + /** + * Returns $value filtered through each filter in the chain + * + * Filters are run in the order in which they were added to the chain (FIFO) + * + * @param mixed $value + * @return mixed + */ + public function filter($value) + { + $valueFiltered = $value; + foreach ($this->_filters as $filter) { + $valueFiltered = $filter->filter($valueFiltered); + } + return $valueFiltered; + } + + /** + * Returns a value filtered through a specified filter class, without requiring separate + * instantiation of the filter object. + * + * The first argument of this method is a data input value, that you would have filtered. + * The second argument is a string, which corresponds to the basename of the filter class, + * relative to the Zend_Filter namespace. This method automatically loads the class, + * creates an instance, and applies the filter() method to the data input. You can also pass + * an array of constructor arguments, if they are needed for the filter class. + * + * @param mixed $value + * @param string $classBaseName + * @param array $args OPTIONAL + * @param array|string $namespaces OPTIONAL + * @return mixed + * @throws Zend_Filter_Exception + */ + public static function get($value, $classBaseName, array $args = array(), $namespaces = array()) + { + require_once 'Zend/Loader.php'; + $namespaces = array_merge(array('Zend_Filter'), (array) $namespaces); + foreach ($namespaces as $namespace) { + $className = $namespace . '_' . ucfirst($classBaseName); + try { + @Zend_Loader::loadClass($className); + } catch (Zend_Exception $ze) { + continue; + } + $class = new ReflectionClass($className); + if ($class->implementsInterface('Zend_Filter_Interface')) { + if ($class->hasMethod('__construct')) { + $object = $class->newInstanceArgs($args); + } else { + $object = $class->newInstance(); + } + return $object->filter($value); + } + } + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("Filter class not found from basename '$classBaseName'"); + } +} diff --git a/lib/Zend/Filter/Alnum.php b/lib/Zend/Filter/Alnum.php new file mode 100644 index 0000000..a50cddd --- /dev/null +++ b/lib/Zend/Filter/Alnum.php @@ -0,0 +1,115 @@ +allowWhiteSpace = (boolean) $allowWhiteSpace; + if (null === self::$_unicodeEnabled) { + self::$_unicodeEnabled = (@preg_match('/\pL/u', 'a')) ? true : false; + } + + if (null === self::$_meansEnglishAlphabet) { + $this->_locale = new Zend_Locale('auto'); + self::$_meansEnglishAlphabet = in_array($this->_locale->getLanguage(), + array('ja', 'ko', 'zh') + ); + } + + } + + /** + * Defined by Zend_Filter_Interface + * + * Returns the string $value, removing all but alphabetic and digit characters + * + * @param string $value + * @return string + */ + public function filter($value) + { + $whiteSpace = $this->allowWhiteSpace ? '\s' : ''; + if (!self::$_unicodeEnabled) { + // POSIX named classes are not supported, use alternative a-zA-Z0-9 match + $pattern = '/[^a-zA-Z0-9' . $whiteSpace . ']/'; + } else if (self::$_meansEnglishAlphabet) { + //The Alphabet means english alphabet. + $pattern = '/[^a-zA-Z0-9' . $whiteSpace . ']/u'; + } else { + //The Alphabet means each language's alphabet. + $pattern = '/[^\p{L}\p{N}' . $whiteSpace . ']/u'; + } + + return preg_replace($pattern, '', (string) $value); + } +} diff --git a/lib/Zend/Filter/Alpha.php b/lib/Zend/Filter/Alpha.php new file mode 100644 index 0000000..adfaafe --- /dev/null +++ b/lib/Zend/Filter/Alpha.php @@ -0,0 +1,115 @@ +allowWhiteSpace = (boolean) $allowWhiteSpace; + if (null === self::$_unicodeEnabled) { + self::$_unicodeEnabled = (@preg_match('/\pL/u', 'a')) ? true : false; + } + + if (null === self::$_meansEnglishAlphabet) { + $this->_locale = new Zend_Locale('auto'); + self::$_meansEnglishAlphabet = in_array($this->_locale->getLanguage(), + array('ja', 'ko', 'zh') + ); + } + + } + + /** + * Defined by Zend_Filter_Interface + * + * Returns the string $value, removing all but alphabetic characters + * + * @param string $value + * @return string + */ + public function filter($value) + { + $whiteSpace = $this->allowWhiteSpace ? '\s' : ''; + if (!self::$_unicodeEnabled) { + // POSIX named classes are not supported, use alternative a-zA-Z match + $pattern = '/[^a-zA-Z' . $whiteSpace . ']/'; + } else if (self::$_meansEnglishAlphabet) { + //The Alphabet means english alphabet. + $pattern = '/[^a-zA-Z' . $whiteSpace . ']/u'; + } else { + //The Alphabet means each language's alphabet. + $pattern = '/[^\p{L}' . $whiteSpace . ']/u'; + } + + return preg_replace($pattern, '', (string) $value); + } +} diff --git a/lib/Zend/Filter/BaseName.php b/lib/Zend/Filter/BaseName.php new file mode 100644 index 0000000..a0652ab --- /dev/null +++ b/lib/Zend/Filter/BaseName.php @@ -0,0 +1,50 @@ +setEncoding($options); + } + } + + /** + * Defined by Zend_Filter_Interface + * + * Does a lowercase on the content of the given file + * + * @param string $value Full path of file to change + * @return string The given $value + * @throws Zend_Filter_Exception + */ + public function filter($value) + { + if (!file_exists($value)) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("File '$value' not found"); + } + + if (!is_writable($value)) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("File '$value' is not writable"); + } + + $content = file_get_contents($value); + if (!$content) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("Problem while reading file '$value'"); + } + + $content = parent::filter($content); + $result = file_put_contents($value, $content); + + if (!$result) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("Problem while writing file '$value'"); + } + + return $value; + } +} diff --git a/lib/Zend/Filter/File/Rename.php b/lib/Zend/Filter/File/Rename.php new file mode 100644 index 0000000..718443f --- /dev/null +++ b/lib/Zend/Filter/File/Rename.php @@ -0,0 +1,282 @@ + Source filename or directory which will be renamed + * 'target' => Target filename or directory, the new name of the sourcefile + * 'overwrite' => Shall existing files be overwritten ? + * + * @param string|array $options Target file or directory to be renamed + * @param string $target Source filename or directory (deprecated) + * @param bool $overwrite Should existing files be overwritten (deprecated) + * @return void + */ + public function __construct($options) + { + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } elseif (is_string($options)) { + $options = array('target' => $options); + } elseif (!is_array($options)) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception('Invalid options argument provided to filter'); + } + + if (1 < func_num_args()) { + trigger_error('Support for multiple arguments is deprecated in favor of a single options array', E_USER_NOTICE); + $argv = func_get_args(); + array_shift($argv); + $source = array_shift($argv); + $overwrite = false; + if (!empty($argv)) { + $overwrite = array_shift($argv); + } + $options['source'] = $source; + $options['overwrite'] = $overwrite; + } + + $this->setFile($options); + } + + /** + * Returns the files to rename and their new name and location + * + * @return array + */ + public function getFile() + { + return $this->_files; + } + + /** + * Sets a new file or directory as target, deleting existing ones + * + * Array accepts the following keys: + * 'source' => Source filename or directory which will be renamed + * 'target' => Target filename or directory, the new name of the sourcefile + * 'overwrite' => Shall existing files be overwritten ? + * + * @param string|array $options Old file or directory to be rewritten + * @return Zend_Filter_File_Rename + */ + public function setFile($options) + { + $this->_files = array(); + $this->addFile($options); + + return $this; + } + + /** + * Adds a new file or directory as target to the existing ones + * + * Array accepts the following keys: + * 'source' => Source filename or directory which will be renamed + * 'target' => Target filename or directory, the new name of the sourcefile + * 'overwrite' => Shall existing files be overwritten ? + * + * @param string|array $options Old file or directory to be rewritten + * @return Zend_Filter_File_Rename + */ + public function addFile($options) + { + if (is_string($options)) { + $options = array('target' => $options); + } elseif (!is_array($options)) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception ('Invalid options to rename filter provided'); + } + + $this->_convertOptions($options); + + return $this; + } + + /** + * Defined by Zend_Filter_Interface + * + * Renames the file $value to the new name set before + * Returns the file $value, removing all but digit characters + * + * @param string $value Full path of file to change + * @return string The new filename which has been set, or false when there were errors + */ + public function filter($value) + { + $file = $this->_getFileName($value); + if ($file['source'] == $file['target']) { + return $value; + } + + if (!file_exists($file['source'])) { + return $value; + } + + if (($file['overwrite'] == true) and (file_exists($file['target']))) { + unlink($file['target']); + } + + if (file_exists($file['target'])) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception(sprintf("File '%s' could not be renamed. It already exists.", $value)); + } + + $result = rename($file['source'], $file['target']); + + if ($result === true) { + return $file['target']; + } + + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception(sprintf("File '%s' could not be renamed. An error occured while processing the file.", $value)); + } + + /** + * Internal method for creating the file array + * Supports single and nested arrays + * + * @param array $options + * @return array + */ + protected function _convertOptions($options) { + $files = array(); + foreach ($options as $key => $value) { + if (is_array($value)) { + $this->_convertOptions($value); + continue; + } + + switch ($key) { + case "source": + $files['source'] = (string) $value; + break; + + case 'target' : + $files['target'] = (string) $value; + break; + + case 'overwrite' : + $files['overwrite'] = (boolean) $value; + break; + + default: + break; + } + } + + if (empty($files)) { + return $this; + } + + if (empty($files['source'])) { + $files['source'] = '*'; + } + + if (empty($files['target'])) { + $files['target'] = '*'; + } + + if (empty($files['overwrite'])) { + $files['overwrite'] = false; + } + + $found = false; + foreach ($this->_files as $key => $value) { + if ($value['source'] == $files['source']) { + $this->_files[$key] = $files; + $found = true; + } + } + + if (!$found) { + $count = count($this->_files); + $this->_files[$count] = $files; + } + + return $this; + } + + /** + * Internal method to resolve the requested source + * and return all other related parameters + * + * @param string $file Filename to get the informations for + * @return array + */ + protected function _getFileName($file) + { + $rename = array(); + foreach ($this->_files as $value) { + if ($value['source'] == '*') { + if (!isset($rename['source'])) { + $rename = $value; + $rename['source'] = $file; + } + } + + if ($value['source'] == $file) { + $rename = $value; + } + } + + if (!isset($rename['source'])) { + return $file; + } + + if (!isset($rename['target']) or ($rename['target'] == '*')) { + $rename['target'] = $rename['source']; + } + + if (is_dir($rename['target'])) { + $name = basename($rename['source']); + $last = $rename['target'][strlen($rename['target']) - 1]; + if (($last != '/') and ($last != '\\')) { + $rename['target'] .= DIRECTORY_SEPARATOR; + } + + $rename['target'] .= $name; + } + + return $rename; + } +} diff --git a/lib/Zend/Filter/File/UpperCase.php b/lib/Zend/Filter/File/UpperCase.php new file mode 100644 index 0000000..9501e9e --- /dev/null +++ b/lib/Zend/Filter/File/UpperCase.php @@ -0,0 +1,84 @@ +setEncoding($options); + } + } + + /** + * Defined by Zend_Filter_Interface + * + * Does a lowercase on the content of the given file + * + * @param string $value Full path of file to change + * @return string The given $value + * @throws Zend_Filter_Exception + */ + public function filter($value) + { + if (!file_exists($value)) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("File '$value' not found"); + } + + if (!is_writable($value)) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("File '$value' is not writable"); + } + + $content = file_get_contents($value); + if (!$content) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("Problem while reading file '$value'"); + } + + $content = parent::filter($content); + $result = file_put_contents($value, $content); + + if (!$result) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("Problem while writing file '$value'"); + } + + return $value; + } +} diff --git a/lib/Zend/Filter/HtmlEntities.php b/lib/Zend/Filter/HtmlEntities.php new file mode 100644 index 0000000..e9887a2 --- /dev/null +++ b/lib/Zend/Filter/HtmlEntities.php @@ -0,0 +1,122 @@ +_quoteStyle = $quoteStyle; + $this->_charSet = $charSet; + } + + /** + * Returns the quoteStyle option + * + * @return integer + */ + public function getQuoteStyle() + { + return $this->_quoteStyle; + } + + /** + * Sets the quoteStyle option + * + * @param integer $quoteStyle + * @return Zend_Filter_HtmlEntities Provides a fluent interface + */ + public function setQuoteStyle($quoteStyle) + { + $this->_quoteStyle = $quoteStyle; + return $this; + } + + /** + * Returns the charSet option + * + * @return string + */ + public function getCharSet() + { + return $this->_charSet; + } + + /** + * Sets the charSet option + * + * @param string $charSet + * @return Zend_Filter_HtmlEntities Provides a fluent interface + */ + public function setCharSet($charSet) + { + $this->_charSet = $charSet; + return $this; + } + + /** + * Defined by Zend_Filter_Interface + * + * Returns the string $value, converting characters to their corresponding HTML entity + * equivalents where they exist + * + * @param string $value + * @return string + */ + public function filter($value) + { + return htmlentities((string) $value, $this->_quoteStyle, $this->_charSet); + } +} diff --git a/lib/Zend/Filter/Inflector.php b/lib/Zend/Filter/Inflector.php new file mode 100644 index 0000000..4e7e493 --- /dev/null +++ b/lib/Zend/Filter/Inflector.php @@ -0,0 +1,497 @@ +setConfig($target); + } else { + if ((null !== $target) && is_string($target)) { + $this->setTarget($target); + } + + if (null !== $rules) { + $this->addRules($rules); + } + + if ($throwTargetExceptionsOn !== null) { + $this->setThrowTargetExceptionsOn($throwTargetExceptionsOn); + } + + if ($targetReplacementIdentifer != '') { + $this->setTargetReplacementIdentifier($targetReplacementIdentifer); + } + } + } + + /** + * Retreive PluginLoader + * + * @return Zend_Loader_PluginLoader_Interface + */ + public function getPluginLoader() + { + if (!$this->_pluginLoader instanceof Zend_Loader_PluginLoader_Interface) { + $this->_pluginLoader = new Zend_Loader_PluginLoader(array('Zend_Filter_' => 'Zend/Filter/'), __CLASS__); + } + + return $this->_pluginLoader; + } + + /** + * Set PluginLoader + * + * @param Zend_Loader_PluginLoader_Interface $pluginLoader + * @return Zend_Filter_Inflector + */ + public function setPluginLoader(Zend_Loader_PluginLoader_Interface $pluginLoader) + { + $this->_pluginLoader = $pluginLoader; + return $this; + } + + /** + * Use Zend_Config object to set object state + * + * @param Zend_Config $config + * @return Zend_Filter_Inflector + */ + public function setConfig(Zend_Config $config) + { + foreach ($config as $key => $value) { + switch ($key) { + case 'target': + $this->setTarget($value); + break; + case 'filterPrefixPath': + if (is_scalar($value)) { + break; + } + $paths = $value->toArray(); + foreach ($paths as $prefix => $path) { + $this->addFilterPrefixPath($prefix, $path); + } + break; + case 'throwTargetExceptionsOn': + $this->setThrowTargetExceptionsOn($value); + break; + case 'targetReplacementIdentifier': + $this->setTargetReplacementIdentifier($value); + break; + case 'rules': + $this->addRules($value->toArray()); + break; + default: + break; + } + } + return $this; + } + + /** + * Convienence method to add prefix and path to PluginLoader + * + * @param string $prefix + * @param string $path + * @return Zend_Filter_Inflector + */ + public function addFilterPrefixPath($prefix, $path) + { + $this->getPluginLoader()->addPrefixPath($prefix, $path); + return $this; + } + + /** + * Set Whether or not the inflector should throw an exception when a replacement + * identifier is still found within an inflected target. + * + * @param bool $throwTargetExceptions + * @return Zend_Filter_Inflector + */ + public function setThrowTargetExceptionsOn($throwTargetExceptionsOn) + { + $this->_throwTargetExceptionsOn = ($throwTargetExceptionsOn == true) ? true : false; + return $this; + } + + /** + * Will exceptions be thrown? + * + * @return bool + */ + public function isThrowTargetExceptionsOn() + { + return $this->_throwTargetExceptionsOn; + } + + /** + * Set the Target Replacement Identifier, by default ':' + * + * @param string $targetReplacementIdentifier + * @return Zend_Filter_Inflector + */ + public function setTargetReplacementIdentifier($targetReplacementIdentifier) + { + $this->_targetReplacementIdentifier = (string) $targetReplacementIdentifier; + return $this; + } + + /** + * Get Target Replacement Identifier + * + * @return string + */ + public function getTargetReplacementIdentifier() + { + return $this->_targetReplacementIdentifier; + } + + /** + * Set a Target + * ex: 'scripts/:controller/:action.:suffix' + * + * @param string + * @return Zend_Filter_Inflector + */ + public function setTarget($target) + { + $this->_target = (string) $target; + return $this; + } + + /** + * Retrieve target + * + * @return string + */ + public function getTarget() + { + return $this->_target; + } + + /** + * Set Target Reference + * + * @param reference $target + * @return Zend_Filter_Inflector + */ + public function setTargetReference(&$target) + { + $this->_target =& $target; + return $this; + } + + /** + * SetRules() is the same as calling addRules() with the exception that it + * clears the rules before adding them. + * + * @param array $rules + * @return Zend_Filter_Inflector + */ + public function setRules(Array $rules) + { + $this->clearRules(); + $this->addRules($rules); + return $this; + } + + /** + * AddRules(): multi-call to setting filter rules. + * + * If prefixed with a ":" (colon), a filter rule will be added. If not + * prefixed, a static replacement will be added. + * + * ex: + * array( + * ':controller' => array('CamelCaseToUnderscore','StringToLower'), + * ':action' => array('CamelCaseToUnderscore','StringToLower'), + * 'suffix' => 'phtml' + * ); + * + * @param array + * @return Zend_Filter_Inflector + */ + public function addRules(Array $rules) + { + $keys = array_keys($rules); + foreach ($keys as $spec) { + if ($spec[0] == ':') { + $this->addFilterRule($spec, $rules[$spec]); + } else { + $this->setStaticRule($spec, $rules[$spec]); + } + } + + return $this; + } + + /** + * Get rules + * + * By default, returns all rules. If a $spec is provided, will return those + * rules if found, false otherwise. + * + * @param string $spec + * @return array|false + */ + public function getRules($spec = null) + { + if (null !== $spec) { + $spec = $this->_normalizeSpec($spec); + if (isset($this->_rules[$spec])) { + return $this->_rules[$spec]; + } + return false; + } + + return $this->_rules; + } + + /** + * getRule() returns a rule set by setFilterRule(), a numeric index must be provided + * + * @param string $spec + * @param int $index + * @return Zend_Filter_Interface|false + */ + public function getRule($spec, $index) + { + $spec = $this->_normalizeSpec($spec); + if (isset($this->_rules[$spec]) && is_array($this->_rules[$spec])) { + if (isset($this->_rules[$spec][$index])) { + return $this->_rules[$spec][$index]; + } + } + return false; + } + + /** + * ClearRules() clears the rules currently in the inflector + * + * @return Zend_Filter_Inflector + */ + public function clearRules() + { + $this->_rules = array(); + return $this; + } + + /** + * Set a filtering rule for a spec. $ruleSet can be a string, Filter object + * or an array of strings or filter objects. + * + * @param string $spec + * @param array|string|Zend_Filter_Interface $ruleSet + * @return Zend_Filter_Inflector + */ + public function setFilterRule($spec, $ruleSet) + { + $spec = $this->_normalizeSpec($spec); + $this->_rules[$spec] = array(); + return $this->addFilterRule($spec, $ruleSet); + } + + /** + * Add a filter rule for a spec + * + * @param mixed $spec + * @param mixed $ruleSet + * @return void + */ + public function addFilterRule($spec, $ruleSet) + { + $spec = $this->_normalizeSpec($spec); + if (!isset($this->_rules[$spec])) { + $this->_rules[$spec] = array(); + } + if (!is_array($ruleSet)) { + $ruleSet = array($ruleSet); + } + foreach ($ruleSet as $rule) { + $this->_rules[$spec][] = $this->_getRule($rule); + } + return $this; + } + + /** + * Set a static rule for a spec. This is a single string value + * + * @param string $name + * @param string $value + * @return Zend_Filter_Inflector + */ + public function setStaticRule($name, $value) + { + $name = $this->_normalizeSpec($name); + $this->_rules[$name] = (string) $value; + return $this; + } + + /** + * Set Static Rule Reference. + * + * This allows a consuming class to pass a property or variable + * in to be referenced when its time to build the output string from the + * target. + * + * @param string $name + * @param mixed $reference + * @return Zend_Filter_Inflector + */ + public function setStaticRuleReference($name, &$reference) + { + $name = $this->_normalizeSpec($name); + $this->_rules[$name] =& $reference; + return $this; + } + + /** + * Inflect + * + * @param string|array $source + * @return string + */ + public function filter($source) + { + // clean source + foreach ( (array) $source as $sourceName => $sourceValue) { + $source[ltrim($sourceName, ':')] = $sourceValue; + } + + $pregQuotedTargetReplacementIdentifier = preg_quote($this->_targetReplacementIdentifier, '#'); + $processedParts = array(); + + foreach ($this->_rules as $ruleName => $ruleValue) { + if (isset($source[$ruleName])) { + if (is_string($ruleValue)) { + // overriding the set rule + $processedParts['#'.$pregQuotedTargetReplacementIdentifier.$ruleName.'#'] = str_replace('\\', '\\\\', $source[$ruleName]); + } elseif (is_array($ruleValue)) { + $processedPart = $source[$ruleName]; + foreach ($ruleValue as $ruleFilter) { + $processedPart = $ruleFilter->filter($processedPart); + } + $processedParts['#'.$pregQuotedTargetReplacementIdentifier.$ruleName.'#'] = str_replace('\\', '\\\\', $processedPart); + } + } elseif (is_string($ruleValue)) { + $processedParts['#'.$pregQuotedTargetReplacementIdentifier.$ruleName.'#'] = str_replace('\\', '\\\\', $ruleValue); + } + } + + // all of the values of processedParts would have been str_replace('\\', '\\\\', ..)'d to disable preg_replace backreferences + $inflectedTarget = preg_replace(array_keys($processedParts), array_values($processedParts), $this->_target); + + if ($this->_throwTargetExceptionsOn && (preg_match('#(?='.$pregQuotedTargetReplacementIdentifier.'[A-Za-z]{1})#', $inflectedTarget) == true)) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception('A replacement identifier ' . $this->_targetReplacementIdentifier . ' was found inside the inflected target, perhaps a rule was not satisfied with a target source? Unsatisfied inflected target: ' . $inflectedTarget); + } + + return $inflectedTarget; + } + + /** + * Normalize spec string + * + * @param string $spec + * @return string + */ + protected function _normalizeSpec($spec) + { + return ltrim((string) $spec, ':&'); + } + + /** + * Resolve named filters and convert them to filter objects. + * + * @param string $rule + * @return Zend_Filter_Interface + */ + protected function _getRule($rule) + { + if ($rule instanceof Zend_Filter_Interface) { + return $rule; + } + + $rule = (string) $rule; + + $className = $this->getPluginLoader()->load($rule); + $ruleObject = new $className(); + if (!$ruleObject instanceof Zend_Filter_Interface) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception('No class named ' . $rule . ' implementing Zend_Filter_Interface could be found'); + } + + return $ruleObject; + } + +} diff --git a/lib/Zend/Filter/Input.php b/lib/Zend/Filter/Input.php new file mode 100644 index 0000000..a6782a3 --- /dev/null +++ b/lib/Zend/Filter/Input.php @@ -0,0 +1,926 @@ + false, + self::BREAK_CHAIN => false, + self::ESCAPE_FILTER => 'HtmlEntities', + self::MISSING_MESSAGE => "Field '%field%' is required by rule '%rule%', but the field is missing", + self::NOT_EMPTY_MESSAGE => "You must give a non-empty value for field '%field%'", + self::PRESENCE => self::PRESENCE_OPTIONAL + ); + + /** + * @var boolean Set to False initially, this is set to True after the + * input data have been processed. Reset to False in setData() method. + */ + protected $_processed = false; + + /** + * @param array $filterRules + * @param array $validatorRules + * @param array $data OPTIONAL + * @param array $options OPTIONAL + */ + public function __construct($filterRules, $validatorRules, array $data = null, array $options = null) + { + if ($options) { + $this->setOptions($options); + } + + $this->_filterRules = (array) $filterRules; + $this->_validatorRules = (array) $validatorRules; + + if ($data) { + $this->setData($data); + } + } + + /** + * @param mixed $namespaces + * @return Zend_Filter_Input + * @deprecated since 1.5.0RC1 - use addFilterPrefixPath() or addValidatorPrefixPath instead. + */ + public function addNamespace($namespaces) + { + if (!is_array($namespaces)) { + $namespaces = array($namespaces); + } + + foreach ($namespaces as $namespace) { + $prefix = $namespace; + $path = str_replace('_', DIRECTORY_SEPARATOR, $prefix); + $this->addFilterPrefixPath($prefix, $path); + $this->addValidatorPrefixPath($prefix, $path); + } + + return $this; + } + + /** + * Add prefix path for all elements + * + * @param string $prefix + * @param string $path + * @return Zend_Filter_Input + */ + public function addFilterPrefixPath($prefix, $path) + { + $this->getPluginLoader(self::FILTER)->addPrefixPath($prefix, $path); + + return $this; + } + + /** + * Add prefix path for all elements + * + * @param string $prefix + * @param string $path + * @return Zend_Filter_Input + */ + public function addValidatorPrefixPath($prefix, $path) + { + $this->getPluginLoader(self::VALIDATE)->addPrefixPath($prefix, $path); + + return $this; + } + + /** + * Set plugin loaders for use with decorators and elements + * + * @param Zend_Loader_PluginLoader_Interface $loader + * @param string $type 'filter' or 'validate' + * @return Zend_Filter_Input + * @throws Zend_Filter_Exception on invalid type + */ + public function setPluginLoader(Zend_Loader_PluginLoader_Interface $loader, $type) + { + $type = strtolower($type); + switch ($type) { + case self::FILTER: + case self::VALIDATE: + $this->_loaders[$type] = $loader; + return $this; + default: + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception(sprintf('Invalid type "%s" provided to setPluginLoader()', $type)); + } + + return $this; + } + + /** + * Retrieve plugin loader for given type + * + * $type may be one of: + * - filter + * - validator + * + * If a plugin loader does not exist for the given type, defaults are + * created. + * + * @param string $type 'filter' or 'validate' + * @return Zend_Loader_PluginLoader_Interface + * @throws Zend_Filter_Exception on invalid type + */ + public function getPluginLoader($type) + { + $type = strtolower($type); + if (!isset($this->_loaders[$type])) { + switch ($type) { + case self::FILTER: + $prefixSegment = 'Zend_Filter_'; + $pathSegment = 'Zend/Filter/'; + break; + case self::VALIDATE: + $prefixSegment = 'Zend_Validate_'; + $pathSegment = 'Zend/Validate/'; + break; + default: + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception(sprintf('Invalid type "%s" provided to getPluginLoader()', $type)); + } + + require_once 'Zend/Loader/PluginLoader.php'; + $this->_loaders[$type] = new Zend_Loader_PluginLoader( + array($prefixSegment => $pathSegment) + ); + } + + return $this->_loaders[$type]; + } + + /** + * @return array + */ + public function getMessages() + { + $this->_process(); + return array_merge($this->_invalidMessages, $this->_missingFields); + } + + /** + * @return array + */ + public function getErrors() + { + $this->_process(); + return $this->_invalidErrors; + } + + /** + * @return array + */ + public function getInvalid() + { + $this->_process(); + return $this->_invalidMessages; + } + + /** + * @return array + */ + public function getMissing() + { + $this->_process(); + return $this->_missingFields; + } + + /** + * @return array + */ + public function getUnknown() + { + $this->_process(); + return $this->_unknownFields; + } + + /** + * @param string $fieldName OPTIONAL + * @return mixed + */ + public function getEscaped($fieldName = null) + { + $this->_process(); + $this->_getDefaultEscapeFilter(); + + if ($fieldName === null) { + return $this->_escapeRecursive($this->_validFields); + } + if (array_key_exists($fieldName, $this->_validFields)) { + return $this->_escapeRecursive($this->_validFields[$fieldName]); + } + return null; + } + + /** + * @param mixed $value + * @return mixed + */ + protected function _escapeRecursive($data) + { + if (!is_array($data)) { + return $this->_getDefaultEscapeFilter()->filter($data); + } + foreach ($data as &$element) { + $element = $this->_escapeRecursive($element); + } + return $data; + } + + /** + * @param string $fieldName OPTIONAL + * @return mixed + */ + public function getUnescaped($fieldName = null) + { + $this->_process(); + if ($fieldName === null) { + return $this->_validFields; + } + if (array_key_exists($fieldName, $this->_validFields)) { + return $this->_validFields[$fieldName]; + } + return null; + } + + /** + * @param string $fieldName + * @return mixed + */ + public function __get($fieldName) + { + return $this->getEscaped($fieldName); + } + + /** + * @return boolean + */ + public function hasInvalid() + { + $this->_process(); + return !(empty($this->_invalidMessages)); + } + + /** + * @return boolean + */ + public function hasMissing() + { + $this->_process(); + return !(empty($this->_missingFields)); + } + + /** + * @return boolean + */ + public function hasUnknown() + { + $this->_process(); + return !(empty($this->_unknownFields)); + } + + /** + * @return boolean + */ + public function hasValid() + { + $this->_process(); + return !(empty($this->_validFields)); + } + + /** + * @param string $fieldName + * @return boolean + */ + public function isValid($fieldName = null) + { + $this->_process(); + if ($fieldName === null) { + return !($this->hasMissing() || $this->hasInvalid()); + } + return array_key_exists($fieldName, $this->_validFields); + } + + /** + * @param string $fieldName + * @return boolean + */ + public function __isset($fieldName) + { + $this->_process(); + return isset($this->_validFields[$fieldName]); + } + + /** + * @return Zend_Filter_Input + * @throws Zend_Filter_Exception + */ + public function process() + { + $this->_process(); + if ($this->hasInvalid()) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("Input has invalid fields"); + } + if ($this->hasMissing()) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("Input has missing fields"); + } + + return $this; + } + + /** + * @param array $data + * @return Zend_Filter_Input + */ + public function setData(array $data) + { + $this->_data = $data; + + /** + * Reset to initial state + */ + $this->_validFields = array(); + $this->_invalidMessages = array(); + $this->_invalidErrors = array(); + $this->_missingFields = array(); + $this->_unknownFields = array(); + + $this->_processed = false; + + return $this; + } + + /** + * @param mixed $escapeFilter + * @return Zend_Filter_Interface + */ + public function setDefaultEscapeFilter($escapeFilter) + { + if (is_string($escapeFilter) || is_array($escapeFilter)) { + $escapeFilter = $this->_getFilter($escapeFilter); + } + if (!$escapeFilter instanceof Zend_Filter_Interface) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception('Escape filter specified does not implement Zend_Filter_Interface'); + } + $this->_defaultEscapeFilter = $escapeFilter; + return $escapeFilter; + } + + /** + * @param array $options + * @return Zend_Filter_Input + * @throws Zend_Filter_Exception if an unknown option is given + */ + public function setOptions(array $options) + { + foreach ($options as $option => $value) { + switch ($option) { + case self::ESCAPE_FILTER: + $this->setDefaultEscapeFilter($value); + break; + case self::INPUT_NAMESPACE: + $this->addNamespace($value); + break; + case self::ALLOW_EMPTY: + case self::BREAK_CHAIN: + case self::MISSING_MESSAGE: + case self::NOT_EMPTY_MESSAGE: + case self::PRESENCE: + $this->_defaults[$option] = $value; + break; + default: + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("Unknown option '$option'"); + break; + } + } + + return $this; + } + + /* + * Protected methods + */ + + /** + * @return void + */ + protected function _filter() + { + foreach ($this->_filterRules as $ruleName => &$filterRule) { + /** + * Make sure we have an array representing this filter chain. + * Don't typecast to (array) because it might be a Zend_Filter object + */ + if (!is_array($filterRule)) { + $filterRule = array($filterRule); + } + + /** + * Filters are indexed by integer, metacommands are indexed by string. + * Pick out the filters. + */ + $filterList = array(); + foreach ($filterRule as $key => $value) { + if (is_int($key)) { + $filterList[] = $value; + } + } + + /** + * Use defaults for filter metacommands. + */ + $filterRule[self::RULE] = $ruleName; + if (!isset($filterRule[self::FIELDS])) { + $filterRule[self::FIELDS] = $ruleName; + } + + /** + * Load all the filter classes and add them to the chain. + */ + if (!isset($filterRule[self::FILTER_CHAIN])) { + $filterRule[self::FILTER_CHAIN] = new Zend_Filter(); + foreach ($filterList as $filter) { + if (is_string($filter) || is_array($filter)) { + $filter = $this->_getFilter($filter); + } + $filterRule[self::FILTER_CHAIN]->addFilter($filter); + } + } + + /** + * If the ruleName is the special wildcard rule, + * then apply the filter chain to all input data. + * Else just process the field named by the rule. + */ + if ($ruleName == self::RULE_WILDCARD) { + foreach (array_keys($this->_data) as $field) { + $this->_filterRule(array_merge($filterRule, array(self::FIELDS => $field))); + } + } else { + $this->_filterRule($filterRule); + } + } + } + + /** + * @param array $filterRule + * @return void + */ + protected function _filterRule(array $filterRule) + { + $field = $filterRule[self::FIELDS]; + if (!array_key_exists($field, $this->_data)) { + return; + } + if (is_array($this->_data[$field])) { + foreach ($this->_data[$field] as $key => $value) { + $this->_data[$field][$key] = $filterRule[self::FILTER_CHAIN]->filter($value); + } + } else { + $this->_data[$field] = + $filterRule[self::FILTER_CHAIN]->filter($this->_data[$field]); + } + } + + /** + * @return Zend_Filter_Interface + */ + protected function _getDefaultEscapeFilter() + { + if ($this->_defaultEscapeFilter !== null) { + return $this->_defaultEscapeFilter; + } + return $this->setDefaultEscapeFilter($this->_defaults[self::ESCAPE_FILTER]); + } + + /** + * @param string $rule + * @param string $field + * @return string + */ + protected function _getMissingMessage($rule, $field) + { + $message = $this->_defaults[self::MISSING_MESSAGE]; + $message = str_replace('%rule%', $rule, $message); + $message = str_replace('%field%', $field, $message); + return $message; + } + + /** + * @return string + */ + protected function _getNotEmptyMessage($rule, $field) + { + $message = $this->_defaults[self::NOT_EMPTY_MESSAGE]; + $message = str_replace('%rule%', $rule, $message); + $message = str_replace('%field%', $field, $message); + return $message; + } + + /** + * @return void + */ + protected function _process() + { + if ($this->_processed === false) { + $this->_filter(); + $this->_validate(); + $this->_processed = true; + } + } + + /** + * @return void + */ + protected function _validate() + { + /** + * Special case: if there are no validators, treat all fields as valid. + */ + if (!$this->_validatorRules) { + $this->_validFields = $this->_data; + $this->_data = array(); + return; + } + + foreach ($this->_validatorRules as $ruleName => &$validatorRule) { + /** + * Make sure we have an array representing this validator chain. + * Don't typecast to (array) because it might be a Zend_Validate object + */ + if (!is_array($validatorRule)) { + $validatorRule = array($validatorRule); + } + + /** + * Validators are indexed by integer, metacommands are indexed by string. + * Pick out the validators. + */ + $validatorList = array(); + foreach ($validatorRule as $key => $value) { + if (is_int($key)) { + $validatorList[] = $value; + } + } + + /** + * Use defaults for validation metacommands. + */ + $validatorRule[self::RULE] = $ruleName; + if (!isset($validatorRule[self::FIELDS])) { + $validatorRule[self::FIELDS] = $ruleName; + } + if (!isset($validatorRule[self::BREAK_CHAIN])) { + $validatorRule[self::BREAK_CHAIN] = $this->_defaults[self::BREAK_CHAIN]; + } + if (!isset($validatorRule[self::PRESENCE])) { + $validatorRule[self::PRESENCE] = $this->_defaults[self::PRESENCE]; + } + if (!isset($validatorRule[self::ALLOW_EMPTY])) { + $validatorRule[self::ALLOW_EMPTY] = $this->_defaults[self::ALLOW_EMPTY]; + } + if (!isset($validatorRule[self::MESSAGES])) { + $validatorRule[self::MESSAGES] = array(); + } else if (!is_array($validatorRule[self::MESSAGES])) { + $validatorRule[self::MESSAGES] = array($validatorRule[self::MESSAGES]); + } else if (!array_intersect_key($validatorList, $validatorRule[self::MESSAGES])) { + $validatorRule[self::MESSAGES] = array($validatorRule[self::MESSAGES]); + } + + /** + * Load all the validator classes and add them to the chain. + */ + if (!isset($validatorRule[self::VALIDATOR_CHAIN])) { + $validatorRule[self::VALIDATOR_CHAIN] = new Zend_Validate(); + $i = 0; + foreach ($validatorList as $validator) { + + if (is_string($validator) || is_array($validator)) { + $validator = $this->_getValidator($validator); + } + if (isset($validatorRule[self::MESSAGES][$i])) { + $value = $validatorRule[self::MESSAGES][$i]; + if (is_array($value)) { + $validator->setMessages($value); + } else { + $validator->setMessage($value); + } + } + + $validatorRule[self::VALIDATOR_CHAIN]->addValidator($validator, $validatorRule[self::BREAK_CHAIN]); + ++$i; + } + $validatorRule[self::VALIDATOR_CHAIN_COUNT] = $i; + } + + /** + * If the ruleName is the special wildcard rule, + * then apply the validator chain to all input data. + * Else just process the field named by the rule. + */ + if ($ruleName == self::RULE_WILDCARD) { + foreach (array_keys($this->_data) as $field) { + $this->_validateRule(array_merge($validatorRule, array(self::FIELDS => $field))); + } + } else { + $this->_validateRule($validatorRule); + } + } + + /** + * Unset fields in $_data that have been added to other arrays. + * We have to wait until all rules have been processed because + * a given field may be referenced by multiple rules. + */ + foreach (array_merge(array_keys($this->_missingFields), array_keys($this->_invalidMessages)) as $rule) { + foreach ((array) $this->_validatorRules[$rule][self::FIELDS] as $field) { + unset($this->_data[$field]); + } + } + foreach ($this->_validFields as $field => $value) { + unset($this->_data[$field]); + } + + /** + * Anything left over in $_data is an unknown field. + */ + $this->_unknownFields = $this->_data; + } + + /** + * @param array $validatorRule + * @return void + */ + protected function _validateRule(array $validatorRule) + { + /** + * Get one or more data values from input, and check for missing fields. + * Apply defaults if fields are missing. + */ + $data = array(); + foreach ((array) $validatorRule[self::FIELDS] as $field) { + if (array_key_exists($field, $this->_data)) { + $data[$field] = $this->_data[$field]; + } else + if (array_key_exists(self::DEFAULT_VALUE, $validatorRule)) { + if (is_array($validatorRule[self::DEFAULT_VALUE])) { + $key = array_search($field, (array) $validatorRule[self::FIELDS]); + if (array_key_exists($key, $validatorRule[self::DEFAULT_VALUE])) { + $data[$field] = $validatorRule[self::DEFAULT_VALUE][$key]; + } + } else { + $data[$field] = $validatorRule[self::DEFAULT_VALUE]; + } + } else + if ($validatorRule[self::PRESENCE] == self::PRESENCE_REQUIRED) { + $this->_missingFields[$validatorRule[self::RULE]][] = + $this->_getMissingMessage($validatorRule[self::RULE], $field); + } + } + + /** + * If any required fields are missing, break the loop. + */ + if (isset($this->_missingFields[$validatorRule[self::RULE]]) && count($this->_missingFields[$validatorRule[self::RULE]]) > 0) { + return; + } + + /** + * Evaluate the inputs against the validator chain. + */ + if (count((array) $validatorRule[self::FIELDS]) > 1) { + if (!$validatorRule[self::VALIDATOR_CHAIN]->isValid($data)) { + $this->_invalidMessages[$validatorRule[self::RULE]] = $validatorRule[self::VALIDATOR_CHAIN]->getMessages(); + $this->_invalidErrors[$validatorRule[self::RULE]] = $validatorRule[self::VALIDATOR_CHAIN]->getErrors(); + return; + } + } else { + $failed = false; + foreach ($data as $fieldKey => $field) { + if (!is_array($field)) { + $field = array($field); + } + foreach ($field as $value) { + if (empty($value)) { + if ($validatorRule[self::ALLOW_EMPTY] == true) { + continue; + } + if ($validatorRule[self::VALIDATOR_CHAIN_COUNT] == 0) { + $notEmptyValidator = $this->_getValidator('NotEmpty'); + $notEmptyValidator->setMessage($this->_getNotEmptyMessage($validatorRule[self::RULE], $fieldKey)); + $validatorRule[self::VALIDATOR_CHAIN]->addValidator($notEmptyValidator); + } + } + if (!$validatorRule[self::VALIDATOR_CHAIN]->isValid($value)) { + $this->_invalidMessages[$validatorRule[self::RULE]] = + $validatorRule[self::VALIDATOR_CHAIN]->getMessages(); + $this->_invalidErrors[$validatorRule[self::RULE]] = + $validatorRule[self::VALIDATOR_CHAIN]->getErrors(); + unset($this->_validFields[$fieldKey]); + $failed = true; + if ($validatorRule[self::BREAK_CHAIN]) { + return; + } + } + } + } + if ($failed) { + return; + } + } + + /** + * If we got this far, the inputs for this rule pass validation. + */ + foreach ((array) $validatorRule[self::FIELDS] as $field) { + if (array_key_exists($field, $data)) { + $this->_validFields[$field] = $data[$field]; + } + } + } + + /** + * @param mixed $classBaseName + * @return Zend_Filter_Interface + */ + protected function _getFilter($classBaseName) + { + return $this->_getFilterOrValidator(self::FILTER, $classBaseName); + } + + /** + * @param mixed $classBaseName + * @return Zend_Validate_Interface + */ + protected function _getValidator($classBaseName) + { + return $this->_getFilterOrValidator(self::VALIDATE, $classBaseName); + } + + /** + * @param string $type + * @param mixed $classBaseName + * @return Zend_Filter_Interface|Zend_Validate_Interface + * @throws Zend_Filter_Exception + */ + protected function _getFilterOrValidator($type, $classBaseName) + { + $args = array(); + + if (is_array($classBaseName)) { + $args = $classBaseName; + $classBaseName = array_shift($args); + } + + $interfaceName = 'Zend_' . ucfirst($type) . '_Interface'; + $className = $this->getPluginLoader($type)->load(ucfirst($classBaseName)); + + $class = new ReflectionClass($className); + + if (!$class->implementsInterface($interfaceName)) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception("Class based on basename '$classBaseName' must implement the '$interfaceName' interface"); + } + + if ($class->hasMethod('__construct')) { + $object = $class->newInstanceArgs($args); + } else { + $object = $class->newInstance(); + } + + return $object; + } + +} diff --git a/lib/Zend/Filter/Int.php b/lib/Zend/Filter/Int.php new file mode 100644 index 0000000..39c3d6d --- /dev/null +++ b/lib/Zend/Filter/Int.php @@ -0,0 +1,50 @@ +setMatchPattern($matchPattern); + } + + if ($replacement) { + $this->setReplacement($replacement); + } + } + + /** + * Set the match pattern for the regex being called within filter() + * + * @param mixed $match - same as the first argument of preg_replace + * @return Zend_Filter_PregReplace + */ + public function setMatchPattern($match) + { + $this->_matchPattern = $match; + return $this; + } + + /** + * Get currently set match pattern + * + * @return string + */ + public function getMatchPattern() + { + return $this->_matchPattern; + } + + /** + * Set the Replacement pattern/string for the preg_replace called in filter + * + * @param mixed $replacement - same as the second argument of preg_replace + * @return Zend_Filter_PregReplace + */ + public function setReplacement($replacement) + { + $this->_replacement = $replacement; + return $this; + } + + /** + * Get currently set replacement value + * + * @return string + */ + public function getReplacement() + { + return $this->_replacement; + } + + /** + * Perform regexp replacement as filter + * + * @param string $value + * @return string + */ + public function filter($value) + { + if ($this->_matchPattern == null) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception(get_class($this) . ' does not have a valid MatchPattern set.'); + } + + return preg_replace($this->_matchPattern, $this->_replacement, $value); + } + +} diff --git a/lib/Zend/Filter/RealPath.php b/lib/Zend/Filter/RealPath.php new file mode 100644 index 0000000..f94de32 --- /dev/null +++ b/lib/Zend/Filter/RealPath.php @@ -0,0 +1,50 @@ +_encoding = $encoding; + } + + /** + * Defined by Zend_Filter_Interface + * + * Returns the string $value, converting characters to lowercase as necessary + * + * @param string $value + * @return string + */ + public function filter($value) + { + if ($this->_encoding) { + return mb_strtolower((string) $value, $this->_encoding); + } + + return strtolower((string) $value); + } +} diff --git a/lib/Zend/Filter/StringToUpper.php b/lib/Zend/Filter/StringToUpper.php new file mode 100644 index 0000000..bd68aae --- /dev/null +++ b/lib/Zend/Filter/StringToUpper.php @@ -0,0 +1,76 @@ +_encoding = $encoding; + } + + /** + * Defined by Zend_Filter_Interface + * + * Returns the string $value, converting characters to uppercase as necessary + * + * @param string $value + * @return string + */ + public function filter($value) + { + if ($this->_encoding) { + return mb_strtoupper((string) $value, $this->_encoding); + } + + return strtoupper((string) $value); + } +} diff --git a/lib/Zend/Filter/StringTrim.php b/lib/Zend/Filter/StringTrim.php new file mode 100644 index 0000000..af22900 --- /dev/null +++ b/lib/Zend/Filter/StringTrim.php @@ -0,0 +1,97 @@ +_charList = $charList; + } + + /** + * Returns the charList option + * + * @return string|null + */ + public function getCharList() + { + return $this->_charList; + } + + /** + * Sets the charList option + * + * @param string|null $charList + * @return Zend_Filter_StringTrim Provides a fluent interface + */ + public function setCharList($charList) + { + $this->_charList = $charList; + return $this; + } + + /** + * Defined by Zend_Filter_Interface + * + * Returns the string $value with characters stripped from the beginning and end + * + * @param string $value + * @return string + */ + public function filter($value) + { + if (null === $this->_charList) { + return trim((string) $value); + } else { + return trim((string) $value, $this->_charList); + } + } +} diff --git a/lib/Zend/Filter/StripNewlines.php b/lib/Zend/Filter/StripNewlines.php new file mode 100644 index 0000000..b2dd042 --- /dev/null +++ b/lib/Zend/Filter/StripNewlines.php @@ -0,0 +1,48 @@ +setTagsAllowed($tagsAllowed); + $this->setAttributesAllowed($attributesAllowed); + $this->commentsAllowed = (boolean) $commentsAllowed; + } + + /** + * Returns the tagsAllowed option + * + * @return array + */ + public function getTagsAllowed() + { + return $this->_tagsAllowed; + } + + /** + * Sets the tagsAllowed option + * + * @param array|string $tagsAllowed + * @return Zend_Filter_StripTags Provides a fluent interface + */ + public function setTagsAllowed($tagsAllowed) + { + if (!is_array($tagsAllowed)) { + $tagsAllowed = array($tagsAllowed); + } + + foreach ($tagsAllowed as $index => $element) { + // If the tag was provided without attributes + if (is_int($index) && is_string($element)) { + // Canonicalize the tag name + $tagName = strtolower($element); + // Store the tag as allowed with no attributes + $this->_tagsAllowed[$tagName] = array(); + } + // Otherwise, if a tag was provided with attributes + else if (is_string($index) && (is_array($element) || is_string($element))) { + // Canonicalize the tag name + $tagName = strtolower($index); + // Canonicalize the attributes + if (is_string($element)) { + $element = array($element); + } + // Store the tag as allowed with the provided attributes + $this->_tagsAllowed[$tagName] = array(); + foreach ($element as $attribute) { + if (is_string($attribute)) { + // Canonicalize the attribute name + $attributeName = strtolower($attribute); + $this->_tagsAllowed[$tagName][$attributeName] = null; + } + } + } + } + + return $this; + } + + /** + * Returns the attributesAllowed option + * + * @return array + */ + public function getAttributesAllowed() + { + return $this->_attributesAllowed; + } + + /** + * Sets the attributesAllowed option + * + * @param array|string $attributesAllowed + * @return Zend_Filter_StripTags Provides a fluent interface + */ + public function setAttributesAllowed($attributesAllowed) + { + if (!is_array($attributesAllowed)) { + $attributesAllowed = array($attributesAllowed); + } + + // Store each attribute as allowed + foreach ($attributesAllowed as $attribute) { + if (is_string($attribute)) { + // Canonicalize the attribute name + $attributeName = strtolower($attribute); + $this->_attributesAllowed[$attributeName] = null; + } + } + + return $this; + } + + /** + * Defined by Zend_Filter_Interface + * + * @todo improve docblock descriptions + * + * @param string $value + * @return string + */ + public function filter($value) + { + $valueCopy = (string) $value; + + // If comments are allowed, then replace them with unique identifiers + if ($this->commentsAllowed) { + preg_match_all('/<\!--.*?--\s*>/s' , (string) $valueCopy, $matches); + $comments = array_unique($matches[0]); + foreach ($comments as $k => $v) { + $valueCopy = str_replace($v, self::UNIQUE_ID_PREFIX . $k, $valueCopy); + } + } + + // Initialize accumulator for filtered data + $dataFiltered = ''; + // Parse the input data iteratively as regular pre-tag text followed by a + // tag; either may be empty strings + preg_match_all('/([^<]*)(]*>?)/', (string) $valueCopy, $matches); + // Iterate over each set of matches + foreach ($matches[1] as $index => $preTag) { + // If the pre-tag text is non-empty, strip any ">" characters from it + if (strlen($preTag)) { + $preTag = str_replace('>', '', $preTag); + } + // If a tag exists in this match, then filter the tag + $tag = $matches[2][$index]; + if (strlen($tag)) { + $tagFiltered = $this->_filterTag($tag); + } else { + $tagFiltered = ''; + } + // Add the filtered pre-tag text and filtered tag to the data buffer + $dataFiltered .= $preTag . $tagFiltered; + } + + // If comments are allowed, then replace the unique identifiers with the corresponding comments + if ($this->commentsAllowed) { + foreach ($comments as $k => $v) { + $dataFiltered = str_replace(self::UNIQUE_ID_PREFIX . $k, $v, $dataFiltered); + } + } + + // Return the filtered data + return $dataFiltered; + } + + /** + * Filters a single tag against the current option settings + * + * @param string $tag + * @return string + */ + protected function _filterTag($tag) + { + // Parse the tag into: + // 1. a starting delimiter (mandatory) + // 2. a tag name (if available) + // 3. a string of attributes (if available) + // 4. an ending delimiter (if available) + $isMatch = preg_match('~()|[^/>])*)(/?>)~', $tag, $matches); + + // If the tag does not match, then strip the tag entirely + if (!$isMatch) { + return ''; + } + + // Save the matches to more meaningfully named variables + $tagStart = $matches[1]; + $tagName = strtolower($matches[2]); + $tagAttributes = $matches[3]; + $tagEnd = $matches[5]; + + // If the tag is not an allowed tag, then remove the tag entirely + if (!isset($this->_tagsAllowed[$tagName])) { + return ''; + } + + // Trim the attribute string of whitespace at the ends + $tagAttributes = trim($tagAttributes); + + // If there are non-whitespace characters in the attribute string + if (strlen($tagAttributes)) { + // Parse iteratively for well-formed attributes + preg_match_all('/(\w+)\s*=\s*(?:(")(.*?)"|(\')(.*?)\')/s', $tagAttributes, $matches); + + // Initialize valid attribute accumulator + $tagAttributes = ''; + + // Iterate over each matched attribute + foreach ($matches[1] as $index => $attributeName) { + $attributeName = strtolower($attributeName); + $attributeDelimiter = $matches[2][$index]; + $attributeValue = $matches[3][$index]; + + // If the attribute is not allowed, then remove it entirely + if (!array_key_exists($attributeName, $this->_tagsAllowed[$tagName]) + && !array_key_exists($attributeName, $this->_attributesAllowed)) { + continue; + } + // Add the attribute to the accumulator + $tagAttributes .= " $attributeName=" . $attributeDelimiter + . $attributeValue . $attributeDelimiter; + } + } + + // Reconstruct tags ending with "/>" as backwards-compatible XHTML tag + if (strpos($tagEnd, '/') !== false) { + $tagEnd = " $tagEnd"; + } + + // Return the filtered tag + return $tagStart . $tagName . $tagAttributes . $tagEnd; + } +} diff --git a/lib/Zend/Filter/Word/CamelCaseToDash.php b/lib/Zend/Filter/Word/CamelCaseToDash.php new file mode 100644 index 0000000..c4717f2 --- /dev/null +++ b/lib/Zend/Filter/Word/CamelCaseToDash.php @@ -0,0 +1,44 @@ +_separator . '\1', $this->_separator . '\1')); + } else { + parent::setMatchPattern(array('#(?<=(?:[A-Z]))([A-Z]+)([A-Z][A-z])#', '#(?<=(?:[a-z]))([A-Z])#')); + parent::setReplacement(array('\1' . $this->_separator . '\2', $this->_separator . '\1')); + } + + return parent::filter($value); + } + +} diff --git a/lib/Zend/Filter/Word/CamelCaseToUnderscore.php b/lib/Zend/Filter/Word/CamelCaseToUnderscore.php new file mode 100644 index 0000000..659dfb4 --- /dev/null +++ b/lib/Zend/Filter/Word/CamelCaseToUnderscore.php @@ -0,0 +1,44 @@ +setMatchPattern('#-#'); + $this->setReplacement($this->_separator); + return parent::filter($value); + } +} diff --git a/lib/Zend/Filter/Word/DashToUnderscore.php b/lib/Zend/Filter/Word/DashToUnderscore.php new file mode 100644 index 0000000..404598a --- /dev/null +++ b/lib/Zend/Filter/Word/DashToUnderscore.php @@ -0,0 +1,45 @@ +setSeparator($separator); + } + + /** + * Sets a new seperator + * + * @param string $separator Seperator + * @return $this + */ + public function setSeparator($separator) + { + if ($separator == null) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception('"' . $separator . '" is not a valid separator.'); + } + $this->_separator = $separator; + return $this; + } + + /** + * Returns the actual set seperator + * + * @return string + */ + public function getSeparator() + { + return $this->_separator; + } + +} \ No newline at end of file diff --git a/lib/Zend/Filter/Word/SeparatorToCamelCase.php b/lib/Zend/Filter/Word/SeparatorToCamelCase.php new file mode 100644 index 0000000..853ec1b --- /dev/null +++ b/lib/Zend/Filter/Word/SeparatorToCamelCase.php @@ -0,0 +1,52 @@ +_separator, '#'); + + if (self::isUnicodeSupportEnabled()) { + parent::setMatchPattern(array('#('.$pregQuotedSeparator.')(\p{L}{1})#e','#(^\p{Ll}{1})#e')); + parent::setReplacement(array("strtoupper('\\2')","strtoupper('\\1')")); + } else { + parent::setMatchPattern(array('#('.$pregQuotedSeparator.')([A-Z]{1})#e','#(^[a-z]{1})#e')); + parent::setReplacement(array("strtoupper('\\2')","strtoupper('\\1')")); + } + + return parent::filter($value); + } + +} diff --git a/lib/Zend/Filter/Word/SeparatorToDash.php b/lib/Zend/Filter/Word/SeparatorToDash.php new file mode 100644 index 0000000..d5eab06 --- /dev/null +++ b/lib/Zend/Filter/Word/SeparatorToDash.php @@ -0,0 +1,46 @@ +setSearchSeparator($searchSeparator); + $this->setReplacementSeparator($replacementSeparator); + } + + /** + * Sets a new seperator to search for + * + * @param string $separator Seperator to search for + * @return $this + */ + public function setSearchSeparator($separator) + { + $this->_searchSeparator = $separator; + return $this; + } + + /** + * Returns the actual set seperator to search for + * + * @return string + */ + public function getSearchSeparator() + { + return $this->_searchSeparator; + } + + /** + * Sets a new seperator which replaces the searched one + * + * @param string $separator Seperator which replaces the searched one + * @return $this + */ + public function setReplacementSeparator($separator) + { + $this->_replacementSeparator = $separator; + return $this; + } + + /** + * Returns the actual set seperator which replaces the searched one + * + * @return string + */ + public function getReplacementSeparator() + { + return $this->_replacementSeparator; + } + + /** + * Defined by Zend_Filter_Interface + * + * Returns the string $value, replacing the searched seperators with the defined ones + * + * @param string $value + * @return string + */ + public function filter($value) + { + return $this->_separatorToSeparatorFilter($value); + } + + /** + * Do the real work, replaces the seperator to search for with the replacement seperator + * + * Returns the replaced string + * + * @param string $value + * @return string + */ + protected function _separatorToSeparatorFilter($value) + { + if ($this->_searchSeparator == null) { + require_once 'Zend/Filter/Exception.php'; + throw new Zend_Filter_Exception('You must provide a search separator for this filter to work.'); + } + + $this->setMatchPattern('#' . preg_quote($this->_searchSeparator, '#') . '#'); + $this->setReplacement($this->_replacementSeparator); + return parent::filter($value); + } + +} \ No newline at end of file diff --git a/lib/Zend/Filter/Word/UnderscoreToCamelCase.php b/lib/Zend/Filter/Word/UnderscoreToCamelCase.php new file mode 100644 index 0000000..aabbba9 --- /dev/null +++ b/lib/Zend/Filter/Word/UnderscoreToCamelCase.php @@ -0,0 +1,44 @@ + $dir) { + if ($dir == '.') { + $dirs[$key] = $dirPath; + } else { + $dir = rtrim($dir, '\\/'); + $dirs[$key] = $dir . DIRECTORY_SEPARATOR . $dirPath; + } + } + $file = basename($file); + self::loadFile($file, $dirs, true); + } else { + self::_securityCheck($file); + include_once $file; + } + + if (!class_exists($class, false) && !interface_exists($class, false)) { + require_once 'Zend/Exception.php'; + throw new Zend_Exception("File \"$file\" does not exist or class \"$class\" was not found in the file"); + } + } + + /** + * Loads a PHP file. This is a wrapper for PHP's include() function. + * + * $filename must be the complete filename, including any + * extension such as ".php". Note that a security check is performed that + * does not permit extended characters in the filename. This method is + * intended for loading Zend Framework files. + * + * If $dirs is a string or an array, it will search the directories + * in the order supplied, and attempt to load the first matching file. + * + * If the file was not found in the $dirs, or if no $dirs were specified, + * it will attempt to load it from PHP's include_path. + * + * If $once is TRUE, it will use include_once() instead of include(). + * + * @param string $filename + * @param string|array $dirs - OPTIONAL either a path or array of paths + * to search. + * @param boolean $once + * @return boolean + * @throws Zend_Exception + */ + public static function loadFile($filename, $dirs = null, $once = false) + { + self::_securityCheck($filename); + + /** + * Search in provided directories, as well as include_path + */ + $incPath = false; + if (!empty($dirs) && (is_array($dirs) || is_string($dirs))) { + if (is_array($dirs)) { + $dirs = implode(PATH_SEPARATOR, $dirs); + } + $incPath = get_include_path(); + set_include_path($dirs . PATH_SEPARATOR . $incPath); + } + + /** + * Try finding for the plain filename in the include_path. + */ + if ($once) { + include_once $filename; + } else { + include $filename; + } + + /** + * If searching in directories, reset include_path + */ + if ($incPath) { + set_include_path($incPath); + } + + return true; + } + + /** + * Returns TRUE if the $filename is readable, or FALSE otherwise. + * This function uses the PHP include_path, where PHP's is_readable() + * does not. + * + * Note from ZF-2900: + * If you use custom error handler, please check whether return value + * from error_reporting() is zero or not. + * At mark of fopen() can not suppress warning if the handler is used. + * + * @param string $filename + * @return boolean + */ + public static function isReadable($filename) + { + if (!$fh = @fopen($filename, 'r', true)) { + return false; + } + @fclose($fh); + return true; + } + + /** + * spl_autoload() suitable implementation for supporting class autoloading. + * + * Attach to spl_autoload() using the following: + * + * spl_autoload_register(array('Zend_Loader', 'autoload')); + * + * + * @param string $class + * @return string|false Class name on success; false on failure + */ + public static function autoload($class) + { + try { + self::loadClass($class); + return $class; + } catch (Exception $e) { + return false; + } + } + + /** + * Register {@link autoload()} with spl_autoload() + * + * @param string $class (optional) + * @param boolean $enabled (optional) + * @return void + * @throws Zend_Exception if spl_autoload() is not found + * or if the specified class does not have an autoload() method. + */ + public static function registerAutoload($class = 'Zend_Loader', $enabled = true) + { + if (!function_exists('spl_autoload_register')) { + require_once 'Zend/Exception.php'; + throw new Zend_Exception('spl_autoload does not exist in this PHP installation'); + } + + self::loadClass($class); + $methods = get_class_methods($class); + if (!in_array('autoload', (array) $methods)) { + require_once 'Zend/Exception.php'; + throw new Zend_Exception("The class \"$class\" does not have an autoload() method"); + } + + if ($enabled === true) { + spl_autoload_register(array($class, 'autoload')); + } else { + spl_autoload_unregister(array($class, 'autoload')); + } + } + + /** + * Ensure that filename does not contain exploits + * + * @param string $filename + * @return void + * @throws Zend_Exception + */ + protected static function _securityCheck($filename) + { + /** + * Security check + */ + if (preg_match('/[^a-z0-9\\/\\\\_.-]/i', $filename)) { + require_once 'Zend/Exception.php'; + throw new Zend_Exception('Security check: Illegal character in filename'); + } + } + + /** + * Attempt to include() the file. + * + * include() is not prefixed with the @ operator because if + * the file is loaded and contains a parse error, execution + * will halt silently and this is difficult to debug. + * + * Always set display_errors = Off on production servers! + * + * @param string $filespec + * @param boolean $once + * @return boolean + * @deprecated Since 1.5.0; use loadFile() instead + */ + protected static function _includeFile($filespec, $once = false) + { + if ($once) { + return include_once $filespec; + } else { + return include $filespec ; + } + } +} diff --git a/lib/Zend/Loader/Exception.php b/lib/Zend/Loader/Exception.php new file mode 100644 index 0000000..2c0e1e4 --- /dev/null +++ b/lib/Zend/Loader/Exception.php @@ -0,0 +1,35 @@ +_useStaticRegistry = $staticRegistryName; + if(!isset(self::$_staticPrefixToPaths[$staticRegistryName])) { + self::$_staticPrefixToPaths[$staticRegistryName] = array(); + } + if(!isset(self::$_staticLoadedPlugins[$staticRegistryName])) { + self::$_staticLoadedPlugins[$staticRegistryName] = array(); + } + } + + foreach ($prefixToPaths as $prefix => $path) { + $this->addPrefixPath($prefix, $path); + } + } + + /** + * Format prefix for internal use + * + * @param string $prefix + * @return string + */ + protected function _formatPrefix($prefix) + { + return rtrim($prefix, '_') . '_'; + } + + /** + * Add prefixed paths to the registry of paths + * + * @param string $prefix + * @param string $path + * @return Zend_Loader_PluginLoader + */ + public function addPrefixPath($prefix, $path) + { + if (!is_string($prefix) || !is_string($path)) { + require_once 'Zend/Loader/PluginLoader/Exception.php'; + throw new Zend_Loader_PluginLoader_Exception('Zend_Loader_PluginLoader::addPrefixPath() method only takes strings for prefix and path.'); + } + + $prefix = $this->_formatPrefix($prefix); + $path = rtrim($path, '/\\') . '/'; + + if ($this->_useStaticRegistry) { + self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix][] = $path; + } else { + $this->_prefixToPaths[$prefix][] = $path; + } + return $this; + } + + /** + * Get path stack + * + * @param string $prefix + * @return false|array False if prefix does not exist, array otherwise + */ + public function getPaths($prefix = null) + { + if ((null !== $prefix) && is_string($prefix)) { + $prefix = $this->_formatPrefix($prefix); + if ($this->_useStaticRegistry) { + if (isset(self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix])) { + return self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix]; + } + + return false; + } + + if (isset($this->_prefixToPaths[$prefix])) { + return $this->_prefixToPaths[$prefix]; + } + + return false; + } + + if ($this->_useStaticRegistry) { + return self::$_staticPrefixToPaths[$this->_useStaticRegistry]; + } + + return $this->_prefixToPaths; + } + + /** + * Clear path stack + * + * @param string $prefix + * @return bool False only if $prefix does not exist + */ + public function clearPaths($prefix = null) + { + if ((null !== $prefix) && is_string($prefix)) { + $prefix = $this->_formatPrefix($prefix); + if ($this->_useStaticRegistry) { + if (isset(self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix])) { + unset(self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix]); + return true; + } + + return false; + } + + if (isset($this->_prefixToPaths[$prefix])) { + unset($this->_prefixToPaths[$prefix]); + return true; + } + + return false; + } + + if ($this->_useStaticRegistry) { + self::$_staticPrefixToPaths[$this->_useStaticRegistry] = array(); + } else { + $this->_prefixToPaths = array(); + } + + return true; + } + + /** + * Remove a prefix (or prefixed-path) from the registry + * + * @param string $prefix + * @param string $path OPTIONAL + * @return Zend_Loader_PluginLoader + */ + public function removePrefixPath($prefix, $path = null) + { + $prefix = $this->_formatPrefix($prefix); + if ($this->_useStaticRegistry) { + $registry =& self::$_staticPrefixToPaths[$this->_useStaticRegistry]; + } else { + $registry =& $this->_prefixToPaths; + } + + if (!isset($registry[$prefix])) { + require_once 'Zend/Loader/PluginLoader/Exception.php'; + throw new Zend_Loader_PluginLoader_Exception('Prefix ' . $prefix . ' was not found in the PluginLoader.'); + } + + if ($path != null) { + $pos = array_search($path, $registry[$prefix]); + if ($pos === null) { + require_once 'Zend/Loader/PluginLoader/Exception.php'; + throw new Zend_Loader_PluginLoader_Exception('Prefix ' . $prefix . ' / Path ' . $path . ' was not found in the PluginLoader.'); + } + unset($registry[$prefix][$pos]); + } else { + unset($registry[$prefix]); + } + + return $this; + } + + /** + * Normalize plugin name + * + * @param string $name + * @return string + */ + protected function _formatName($name) + { + return ucfirst((string) $name); + } + + /** + * Whether or not a Plugin by a specific name is loaded + * + * @param string $name + * @return Zend_Loader_PluginLoader + */ + public function isLoaded($name) + { + $name = $this->_formatName($name); + if ($this->_useStaticRegistry) { + return isset(self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name]); + } + + return isset($this->_loadedPlugins[$name]); + } + + /** + * Return full class name for a named plugin + * + * @param string $name + * @return string|false False if class not found, class name otherwise + */ + public function getClassName($name) + { + $name = $this->_formatName($name); + if ($this->_useStaticRegistry + && isset(self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name]) + ) { + return self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name]; + } elseif (isset($this->_loadedPlugins[$name])) { + return $this->_loadedPlugins[$name]; + } + + return false; + } + + /** + * Get path to plugin class + * + * @param mixed $name + * @return string|false False if not found + */ + public function getClassPath($name) + { + $name = $this->_formatName($name); + if ($this->_useStaticRegistry + && !empty(self::$_staticLoadedPluginPaths[$this->_useStaticRegistry][$name]) + ) { + return self::$_staticLoadedPluginPaths[$this->_useStaticRegistry][$name]; + } elseif (!empty($this->_loadedPluginPaths[$name])) { + return $this->_loadedPluginPaths[$name]; + } + + if ($this->isLoaded($name)) { + $class = $this->getClassName($name); + $r = new ReflectionClass($class); + $path = $r->getFileName(); + if ($this->_useStaticRegistry) { + self::$_staticLoadedPluginPaths[$this->_useStaticRegistry][$name] = $path; + } else { + $this->_loadedPluginPaths[$name] = $path; + } + return $path; + } + + return false; + } + + /** + * Load a plugin via the name provided + * + * @param string $name + * @return string Class name of loaded class + * @throws Zend_Loader_Exception if class not found + */ + public function load($name) + { + $name = $this->_formatName($name); + if ($this->isLoaded($name)) { + return $this->getClassName($name); + } + + if ($this->_useStaticRegistry) { + $registry = self::$_staticPrefixToPaths[$this->_useStaticRegistry]; + } else { + $registry = $this->_prefixToPaths; + } + + $registry = array_reverse($registry, true); + $found = false; + $classFile = str_replace('_', DIRECTORY_SEPARATOR, $name) . '.php'; + $incFile = self::getIncludeFileCache(); + foreach ($registry as $prefix => $paths) { + $className = $prefix . $name; + + if (class_exists($className, false)) { + $found = true; + break; + } + + $paths = array_reverse($paths, true); + + foreach ($paths as $path) { + $loadFile = $path . $classFile; + if (Zend_Loader::isReadable($loadFile)) { + include_once $loadFile; + if (class_exists($className, false)) { + if (null !== $incFile) { + self::_appendIncFile($loadFile); + } + $found = true; + break 2; + } + } + } + } + + if (!$found) { + $message = "Plugin by name '$name' was not found in the registry; used paths:"; + foreach ($registry as $prefix => $paths) { + $message .= "\n$prefix: " . implode(PATH_SEPARATOR, $paths); + } + require_once 'Zend/Loader/PluginLoader/Exception.php'; + throw new Zend_Loader_PluginLoader_Exception($message); + } + + if ($this->_useStaticRegistry) { + self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name] = $className; + self::$_staticLoadedPluginPaths[$this->_useStaticRegistry][$name] = (isset($loadFile) ? $loadFile : ''); + } else { + $this->_loadedPlugins[$name] = $className; + $this->_loadedPluginPaths[$name] = (isset($loadFile) ? $loadFile : ''); + } + return $className; + } + + /** + * Set path to class file cache + * + * Specify a path to a file that will add include_once statements for each + * plugin class loaded. This is an opt-in feature for performance purposes. + * + * @param string $file + * @return void + * @throws Zend_Loader_PluginLoader_Exception if file is not writeable or path does not exist + */ + public static function setIncludeFileCache($file) + { + if (null === $file) { + self::$_includeFileCache = null; + return; + } + + if (!file_exists($file) && !file_exists(dirname($file))) { + require_once 'Zend/Loader/PluginLoader/Exception.php'; + throw new Zend_Loader_PluginLoader_Exception('Specified file does not exist and/or directory does not exist (' . $file . ')'); + } + if (file_exists($file) && !is_writable($file)) { + require_once 'Zend/Loader/PluginLoader/Exception.php'; + throw new Zend_Loader_PluginLoader_Exception('Specified file is not writeable (' . $file . ')'); + } + if (!file_exists($file) && file_exists(dirname($file)) && !is_writable(dirname($file))) { + require_once 'Zend/Loader/PluginLoader/Exception.php'; + throw new Zend_Loader_PluginLoader_Exception('Specified file is not writeable (' . $file . ')'); + } + + self::$_includeFileCache = $file; + } + + /** + * Retrieve class file cache path + * + * @return string|null + */ + public static function getIncludeFileCache() + { + return self::$_includeFileCache; + } + + /** + * Append an include_once statement to the class file cache + * + * @param string $incFile + * @return void + */ + protected static function _appendIncFile($incFile) + { + if (!file_exists(self::$_includeFileCache)) { + $file = '_charset = $charset; + } + + /** + * Return charset string + * + * @return string + */ + public function getCharset() + { + return $this->_charset; + } + + /** + * Set content type + * + * Should only be used for manually setting multipart content types. + * + * @param string $type Content type + * @return Zend_Mail Implements fluent interface + * @throws Zend_Mail_Exception for types not supported by Zend_Mime + */ + public function setType($type) + { + $allowed = array( + Zend_Mime::MULTIPART_ALTERNATIVE, + Zend_Mime::MULTIPART_MIXED, + Zend_Mime::MULTIPART_RELATED, + ); + if (!in_array($type, $allowed)) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('Invalid content type "' . $type . '"'); + } + + $this->_type = $type; + return $this; + } + + /** + * Get content type of the message + * + * @return string + */ + public function getType() + { + return $this->_type; + } + + /** + * Set an arbitrary mime boundary for the message + * + * If not set, Zend_Mime will generate one. + * + * @param string $boundary + * @return Zend_Mail Provides fluent interface + */ + public function setMimeBoundary($boundary) + { + $this->_mimeBoundary = $boundary; + + return $this; + } + + /** + * Return the boundary string used for the message + * + * @return string + */ + public function getMimeBoundary() + { + return $this->_mimeBoundary; + } + + /** + * Return encoding of mail headers + * + * @deprecated use {@link getHeaderEncoding()} instead + * @return + */ + public function getEncodingOfHeaders() + { + return $this->getHeaderEncoding(); + } + + /** + * Return the encoding of mail headers + * + * Either Zend_Mime::ENCODING_QUOTEDPRINTABLE or Zend_Mime::ENCODING_BASE64 + * + * @return string + */ + public function getHeaderEncoding() + { + return $this->_headerEncoding; + } + + /** + * Set the encoding of mail headers + * + * @deprecated Use {@link setHeaderEncoding()} instead. + * @param string $encoding + * @return Zend_Mail + */ + public function setEncodingOfHeaders($encoding) + { + return $this->setHeaderEncoding($encoding); + } + + /** + * Set the encoding of mail headers + * + * @param string $encoding Zend_Mime::ENCODING_QUOTEDPRINTABLE or Zend_Mime::ENCODING_BASE64 + * @return Zend_Mail Provides fluent interface + */ + public function setHeaderEncoding($encoding) + { + $allowed = array( + Zend_Mime::ENCODING_BASE64, + Zend_Mime::ENCODING_QUOTEDPRINTABLE + ); + if (!in_array($encoding, $allowed)) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('Invalid encoding "' . $encoding . '"'); + } + $this->_headerEncoding = $encoding; + + return $this; + } + + /** + * Sets the text body for the message. + * + * @param string $txt + * @param string $charset + * @param string $encoding + * @return Zend_Mail Provides fluent interface + */ + public function setBodyText($txt, $charset = null, $encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE) + { + if ($charset === null) { + $charset = $this->_charset; + } + + $mp = new Zend_Mime_Part($txt); + $mp->encoding = $encoding; + $mp->type = Zend_Mime::TYPE_TEXT; + $mp->disposition = Zend_Mime::DISPOSITION_INLINE; + $mp->charset = $charset; + + $this->_bodyText = $mp; + + return $this; + } + + /** + * Return text body Zend_Mime_Part or string + * + * @param bool textOnly Whether to return just the body text content or the MIME part; defaults to false, the MIME part + * @return false|Zend_Mime_Part|string + */ + public function getBodyText($textOnly = false) + { + if ($textOnly && $this->_bodyText) { + $body = $this->_bodyText; + return $body->getContent(); + } + + return $this->_bodyText; + } + + /** + * Sets the HTML body for the message + * + * @param string $html + * @param string $charset + * @param string $encoding + * @return Zend_Mail Provides fluent interface + */ + public function setBodyHtml($html, $charset = null, $encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE) + { + if ($charset === null) { + $charset = $this->_charset; + } + + $mp = new Zend_Mime_Part($html); + $mp->encoding = $encoding; + $mp->type = Zend_Mime::TYPE_HTML; + $mp->disposition = Zend_Mime::DISPOSITION_INLINE; + $mp->charset = $charset; + + $this->_bodyHtml = $mp; + + return $this; + } + + /** + * Return Zend_Mime_Part representing body HTML + * + * @param bool $htmlOnly Whether to return the body HTML only, or the MIME part; defaults to false, the MIME part + * @return false|Zend_Mime_Part|string + */ + public function getBodyHtml($htmlOnly = false) + { + if ($htmlOnly && $this->_bodyHtml) { + $body = $this->_bodyHtml; + return $body->getContent(); + } + + return $this->_bodyHtml; + } + + /** + * Adds an existing attachment to the mail message + * + * @param Zend_Mime_Part $attachment + * @return Zend_Mail Provides fluent interface + */ + public function addAttachment(Zend_Mime_Part $attachment) + { + $this->addPart($attachment); + $this->hasAttachments = true; + + return $this; + } + + /** + * Creates a Zend_Mime_Part attachment + * + * Attachment is automatically added to the mail object after creation. The + * attachment object is returned to allow for further manipulation. + * + * @param string $body + * @param string $mimeType + * @param string $disposition + * @param string $encoding + * @param string $filename OPTIONAL A filename for the attachment + * @return Zend_Mime_Part Newly created Zend_Mime_Part object (to allow + * advanced settings) + */ + public function createAttachment($body, + $mimeType = Zend_Mime::TYPE_OCTETSTREAM, + $disposition = Zend_Mime::DISPOSITION_ATTACHMENT, + $encoding = Zend_Mime::ENCODING_BASE64, + $filename = null) + { + + $mp = new Zend_Mime_Part($body); + $mp->encoding = $encoding; + $mp->type = $mimeType; + $mp->disposition = $disposition; + $mp->filename = $filename; + + $this->addAttachment($mp); + + return $mp; + } + + /** + * Return a count of message parts + * + * @return integer + */ + public function getPartCount() + { + return count($this->_parts); + } + + /** + * Encode header fields + * + * Encodes header content according to RFC1522 if it contains non-printable + * characters. + * + * @param string $value + * @return string + */ + protected function _encodeHeader($value) + { + if (Zend_Mime::isPrintable($value) === false) { + if ($this->getHeaderEncoding() === Zend_Mime::ENCODING_QUOTEDPRINTABLE) { + $value = Zend_Mime::encodeQuotedPrintableHeader($value, $this->getCharset(), Zend_Mime::LINELENGTH, Zend_Mime::LINEEND); + } else { + $value = Zend_Mime::encodeBase64Header($value, $this->getCharset(), Zend_Mime::LINELENGTH, Zend_Mime::LINEEND); + } + } + + return $value; + } + + /** + * Add a header to the message + * + * Adds a header to this message. If append is true and the header already + * exists, raises a flag indicating that the header should be appended. + * + * @param string $headerName + * @param string $value + * @param bool $append + */ + protected function _storeHeader($headerName, $value, $append = false) + { + if (isset($this->_headers[$headerName])) { + $this->_headers[$headerName][] = $value; + } else { + $this->_headers[$headerName] = array($value); + } + + if ($append) { + $this->_headers[$headerName]['append'] = true; + } + + } + + /** + * Clear header from the message + * + * @param string $headerName + */ + protected function _clearHeader($headerName) + { + if (isset($this->_headers[$headerName])){ + unset($this->_headers[$headerName]); + } + } + + /** + * Helper function for adding a recipient and the corresponding header + * + * @param string $headerName + * @param string $email + * @param string $name + */ + protected function _addRecipientAndHeader($headerName, $email, $name) + { + $email = $this->_filterEmail($email); + $name = $this->_filterName($name); + // prevent duplicates + $this->_recipients[$email] = 1; + $this->_storeHeader($headerName, $this->_formatAddress($email, $name), true); + } + + /** + * Adds To-header and recipient + * + * @param string $email + * @param string $name + * @return Zend_Mail Provides fluent interface + */ + public function addTo($email, $name='') + { + $this->_addRecipientAndHeader('To', $email, $name); + $this->_to[] = $email; + return $this; + } + + /** + * Adds Cc-header and recipient + * + * @param string $email + * @param string $name + * @return Zend_Mail Provides fluent interface + */ + public function addCc($email, $name='') + { + $this->_addRecipientAndHeader('Cc', $email, $name); + return $this; + } + + /** + * Adds Bcc recipient + * + * @param string $email + * @return Zend_Mail Provides fluent interface + */ + public function addBcc($email) + { + $this->_addRecipientAndHeader('Bcc', $email, ''); + return $this; + } + + /** + * Return list of recipient email addresses + * + * @return array (of strings) + */ + public function getRecipients() + { + return array_keys($this->_recipients); + } + + /** + * Clears list of recipient email addresses + * + * @return Zend_Mail Provides fluent interface + */ + public function clearRecipients() + { + $this->_recipients = array(); + $this->_to = array(); + + $this->_clearHeader('To'); + $this->_clearHeader('Cc'); + $this->_clearHeader('Bcc'); + + return $this; + } + + /** + * Sets From-header and sender of the message + * + * @param string $email + * @param string $name + * @return Zend_Mail Provides fluent interface + * @throws Zend_Mail_Exception if called subsequent times + */ + public function setFrom($email, $name = null) + { + if ($this->_from === null) { + $email = $this->_filterEmail($email); + $name = $this->_filterName($name); + $this->_from = $email; + $this->_storeHeader('From', $this->_formatAddress($email, $name), true); + } else { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('From Header set twice'); + } + return $this; + } + + /** + * Returns the sender of the mail + * + * @return string + */ + public function getFrom() + { + return $this->_from; + } + + /** + * Clears the sender from the mail + * + * @return Zend_Mail Provides fluent interface + */ + public function clearFrom() + { + $this->_from = null; + $this->_clearHeader('From'); + + return $this; + } + + /** + * Sets the Return-Path header of the message + * + * @param string $email + * @return Zend_Mail Provides fluent interface + * @throws Zend_Mail_Exception if set multiple times + */ + public function setReturnPath($email) + { + if ($this->_returnPath === null) { + $email = $this->_filterEmail($email); + $this->_returnPath = $email; + $this->_storeHeader('Return-Path', $email, false); + } else { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('Return-Path Header set twice'); + } + return $this; + } + + /** + * Returns the current Return-Path address of the message + * + * If no Return-Path header is set, returns the value of {@link $_from}. + * + * @return string + */ + public function getReturnPath() + { + if (null !== $this->_returnPath) { + return $this->_returnPath; + } + + return $this->_from; + } + + /** + * Clears the current Return-Path address from the message + * + * @return Zend_Mail Provides fluent interface + */ + public function clearReturnPath() + { + $this->_returnPath = null; + $this->_clearHeader('Return-Path'); + + return $this; + } + + /** + * Sets the subject of the message + * + * @param string $subject + * @return Zend_Mail Provides fluent interface + * @throws Zend_Mail_Exception + */ + public function setSubject($subject) + { + if ($this->_subject === null) { + $subject = $this->_filterOther($subject); + $this->_subject = $this->_encodeHeader($subject); + $this->_storeHeader('Subject', $this->_subject); + } else { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('Subject set twice'); + } + return $this; + } + + /** + * Returns the encoded subject of the message + * + * @return string + */ + public function getSubject() + { + return $this->_subject; + } + + /** + * Clears the encoded subject from the message + * + * @return Zend_Mail Provides fluent interface + */ + public function clearSubject() + { + $this->_subject = null; + $this->_clearHeader('Subject'); + + return $this; + } + + /** + * Sets Date-header + * + * @param string $date + * @return Zend_Mail Provides fluent interface + * @throws Zend_Mail_Exception if called subsequent times + */ + public function setDate($date = null) + { + if ($this->_date === null) { + if ($date === null) { + $date = date('r'); + } else if (is_int($date)) { + $date = date('r', $date); + } else if (is_string($date)) { + $date = strtotime($date); + if ($date === false || $date < 0) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('String representations of Date Header must be ' . + 'strtotime()-compatible'); + } + $date = date('r', $date); + } else if ($date instanceof Zend_Date) { + $date = $date->get(Zend_Date::RFC_2822); + } else { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception(__METHOD__ . ' only accepts UNIX timestamps, Zend_Date objects, ' . + ' and strtotime()-compatible strings'); + } + $this->_date = $date; + $this->_storeHeader('Date', $date); + } else { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('Date Header set twice'); + } + return $this; + } + + /** + * Returns the formatted date of the message + * + * @return string + */ + public function getDate() + { + return $this->_date; + } + + /** + * Clears the formatted date from the message + * + * @return Zend_Mail Provides fluent interface + */ + public function clearDate() + { + $this->_date = null; + $this->_clearHeader('Date'); + + return $this; + } + + /** + * Sets the Message-ID of the message + * + * @param boolean|string $id + * true :Auto + * false :No set + * null :No set + * string:Sets string + * @return Zend_Mail Provides fluent interface + * @throws Zend_Mail_Exception + */ + public function setMessageId($id = true) + { + if ($id === null || $id === false) { + return $this; + } elseif ($id === true) { + $id = $this->createMessageId(); + } + + if ($this->_messageId === null) { + $id = $this->_filterOther($id); + $this->_messageId = $id; + $this->_storeHeader('Message-Id', $this->_messageId); + } else { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('Message-ID set twice'); + } + + return $this; + } + + /** + * Returns the Message-ID of the message + * + * @return string + */ + public function getMessageId() + { + return $this->_messageId; + } + + + /** + * Clears the Message-ID from the message + * + * @return Zend_Mail Provides fluent interface + */ + public function clearMessageId() + { + $this->_messageId = null; + $this->_clearHeader('Message-Id'); + + return $this; + } + + /** + * Creates the Message-ID + * + * @return string + */ + public function createMessageId() { + + $time = time(); + + if ($this->_from !== null) { + $user = $this->_from; + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $user = $_SERVER['REMOTE_ADDR']; + } else { + $user = getmypid(); + } + + $rand = mt_rand(); + + if ($this->_recipients !== array()) { + $recipient = array_rand($this->_recipients); + } else { + $recipient = 'unknown'; + } + + if (isset($_SERVER["SERVER_NAME"])) { + $hostName = $_SERVER["SERVER_NAME"]; + } else { + $hostName = php_uname('n'); + } + + return sha1($time . $user . $rand . $recipient) . '@' . $hostName; + } + + /** + * Add a custom header to the message + * + * @param string $name + * @param string $value + * @param boolean $append + * @return Zend_Mail Provides fluent interface + * @throws Zend_Mail_Exception on attempts to create standard headers + */ + public function addHeader($name, $value, $append = false) + { + $prohibit = array('to', 'cc', 'bcc', 'from', 'subject', + 'return-path', 'date', 'message-id', + ); + if (in_array(strtolower($name), $prohibit)) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('Cannot set standard header from addHeader()'); + } + + $value = $this->_filterOther($value); + $value = $this->_encodeHeader($value); + $this->_storeHeader($name, $value, $append); + + return $this; + } + + /** + * Return mail headers + * + * @return void + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Sends this email using the given transport or a previously + * set DefaultTransport or the internal mail function if no + * default transport had been set. + * + * @param Zend_Mail_Transport_Abstract $transport + * @return Zend_Mail Provides fluent interface + */ + public function send($transport = null) + { + if ($transport === null) { + if (! self::$_defaultTransport instanceof Zend_Mail_Transport_Abstract) { + require_once 'Zend/Mail/Transport/Sendmail.php'; + $transport = new Zend_Mail_Transport_Sendmail(); + } else { + $transport = self::$_defaultTransport; + } + } + + if ($this->_date === null) { + $this->setDate(); + } + + $transport->send($this); + + return $this; + } + + /** + * Filter of email data + * + * @param string $email + * @return string + */ + protected function _filterEmail($email) + { + $rule = array("\r" => '', + "\n" => '', + "\t" => '', + '"' => '', + ',' => '', + '<' => '', + '>' => '', + ); + + return strtr($email, $rule); + } + + /** + * Filter of name data + * + * @param string $name + * @return string + */ + protected function _filterName($name) + { + $rule = array("\r" => '', + "\n" => '', + "\t" => '', + '"' => "'", + '<' => '[', + '>' => ']', + ); + + return trim(strtr($name, $rule)); + } + + /** + * Filter of other data + * + * @param string $data + * @return string + */ + protected function _filterOther($data) + { + $rule = array("\r" => '', + "\n" => '', + "\t" => '', + ); + + return strtr($data, $rule); + } + + /** + * Formats e-mail address + * + * @param string $email + * @param string $name + * @return string + */ + protected function _formatAddress($email, $name) + { + if ($name === '' || $name === null || $name === $email) { + return $email; + } else { + $encodedName = $this->_encodeHeader($name); + if ($encodedName === $name && strpos($name, ',') !== false) { + $format = '"%s" <%s>'; + } else { + $format = '%s <%s>'; + } + return sprintf($format, $encodedName, $email); + } + } + +} diff --git a/lib/Zend/Mail/Exception.php b/lib/Zend/Mail/Exception.php new file mode 100644 index 0000000..bab758e --- /dev/null +++ b/lib/Zend/Mail/Exception.php @@ -0,0 +1,37 @@ +_flags = array_combine($params['flags'], $params['flags']); + } + + parent::__construct($params); + } + + /** + * return toplines as found after headers + * + * @return string toplines + */ + public function getTopLines() + { + return $this->_topLines; + } + + /** + * check if flag is set + * + * @param mixed $flag a flag name, use constants defined in Zend_Mail_Storage + * @return bool true if set, otherwise false + */ + public function hasFlag($flag) + { + return isset($this->_flags[$flag]); + } + + /** + * get all set flags + * + * @return array array with flags, key and value are the same for easy lookup + */ + public function getFlags() + { + return $this->_flags; + } +} diff --git a/lib/Zend/Mail/Message/File.php b/lib/Zend/Mail/Message/File.php new file mode 100644 index 0000000..5a93449 --- /dev/null +++ b/lib/Zend/Mail/Message/File.php @@ -0,0 +1,96 @@ +_flags = array_combine($params['flags'], $params['flags']); + } + + parent::__construct($params); + } + + /** + * return toplines as found after headers + * + * @return string toplines + */ + public function getTopLines() + { + return $this->_topLines; + } + + /** + * check if flag is set + * + * @param mixed $flag a flag name, use constants defined in Zend_Mail_Storage + * @return bool true if set, otherwise false + */ + public function hasFlag($flag) + { + return isset($this->_flags[$flag]); + } + + /** + * get all set flags + * + * @return array array with flags, key and value are the same for easy lookup + */ + public function getFlags() + { + return $this->_flags; + } +} diff --git a/lib/Zend/Mail/Message/Interface.php b/lib/Zend/Mail/Message/Interface.php new file mode 100644 index 0000000..92acd49 --- /dev/null +++ b/lib/Zend/Mail/Message/Interface.php @@ -0,0 +1,55 @@ + value) or string, if a content part is found it's used as toplines + * - noToplines ignore content found after headers in param 'headers' + * - content content as string + * + * @param array $params full message with or without headers + * @throws Zend_Mail_Exception + */ + public function __construct(array $params) + { + if (isset($params['handler'])) { + if (!$params['handler'] instanceof Zend_Mail_Storage_Abstract) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('handler is not a valid mail handler'); + } + if (!isset($params['id'])) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('need a message id with a handler'); + } + + $this->_mail = $params['handler']; + $this->_messageNum = $params['id']; + } + + if (isset($params['raw'])) { + Zend_Mime_Decode::splitMessage($params['raw'], $this->_headers, $this->_content); + } else if (isset($params['headers'])) { + if (is_array($params['headers'])) { + $this->_headers = $params['headers']; + } else { + if (!empty($params['noToplines'])) { + Zend_Mime_Decode::splitMessage($params['headers'], $this->_headers, $null); + } else { + Zend_Mime_Decode::splitMessage($params['headers'], $this->_headers, $this->_topLines); + } + } + if (isset($params['content'])) { + $this->_content = $params['content']; + } + } + } + + /** + * Check if part is a multipart message + * + * @return bool if part is multipart + */ + public function isMultipart() + { + try { + return stripos($this->contentType, 'multipart/') === 0; + } catch(Zend_Mail_Exception $e) { + return false; + } + } + + + /** + * Body of part + * + * If part is multipart the raw content of this part with all sub parts is returned + * + * @return string body + * @throws Zend_Mail_Exception + */ + public function getContent() + { + if ($this->_content !== null) { + return $this->_content; + } + + if ($this->_mail) { + return $this->_mail->getRawContent($this->_messageNum); + } else { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('no content'); + } + } + + /** + * Return size of part + * + * Quite simple implemented currently (not decoding). Handle with care. + * + * @return int size + */ + public function getSize() { + return strlen($this->getContent()); + } + + + /** + * Cache content and split in parts if multipart + * + * @return null + * @throws Zend_Mail_Exception + */ + protected function _cacheContent() + { + // caching content if we can't fetch parts + if ($this->_content === null && $this->_mail) { + $this->_content = $this->_mail->getRawContent($this->_messageNum); + } + + if (!$this->isMultipart()) { + return; + } + + // split content in parts + $boundary = $this->getHeaderField('content-type', 'boundary'); + if (!$boundary) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('no boundary found in content type to split message'); + } + $parts = Zend_Mime_Decode::splitMessageStruct($this->_content, $boundary); + if ($parts === null) { + return; + } + $counter = 1; + foreach ($parts as $part) { + $this->_parts[$counter++] = new self(array('headers' => $part['header'], 'content' => $part['body'])); + } + } + + /** + * Get part of multipart message + * + * @param int $num number of part starting with 1 for first part + * @return Zend_Mail_Part wanted part + * @throws Zend_Mail_Exception + */ + public function getPart($num) + { + if (isset($this->_parts[$num])) { + return $this->_parts[$num]; + } + + if (!$this->_mail && $this->_content === null) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('part not found'); + } + + if ($this->_mail && $this->_mail->hasFetchPart) { + // TODO: fetch part + // return + } + + $this->_cacheContent(); + + if (!isset($this->_parts[$num])) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('part not found'); + } + + return $this->_parts[$num]; + } + + /** + * Count parts of a multipart part + * + * @return int number of sub-parts + */ + public function countParts() + { + if ($this->_countParts) { + return $this->_countParts; + } + + $this->_countParts = count($this->_parts); + if ($this->_countParts) { + return $this->_countParts; + } + + if ($this->_mail && $this->_mail->hasFetchPart) { + // TODO: fetch part + // return + } + + $this->_cacheContent(); + + $this->_countParts = count($this->_parts); + return $this->_countParts; + } + + + /** + * Get all headers + * + * The returned headers are as saved internally. All names are lowercased. The value is a string or an array + * if a header with the same name occurs more than once. + * + * @return array headers as array(name => value) + */ + public function getHeaders() + { + if ($this->_headers === null) { + if (!$this->_mail) { + $this->_headers = array(); + } else { + $part = $this->_mail->getRawHeader($this->_messageNum); + Zend_Mime_Decode::splitMessage($part, $this->_headers, $null); + } + } + + return $this->_headers; + } + + /** + * Get a header in specificed format + * + * Internally headers that occur more than once are saved as array, all other as string. If $format + * is set to string implode is used to concat the values (with Zend_Mime::LINEEND as delim). + * + * @param string $name name of header, matches case-insensitive, but camel-case is replaced with dashes + * @param string $format change type of return value to 'string' or 'array' + * @return string|array value of header in wanted or internal format + * @throws Zend_Mail_Exception + */ + public function getHeader($name, $format = null) + { + if ($this->_headers === null) { + $this->getHeaders(); + } + + $lowerName = strtolower($name); + + if (!isset($this->_headers[$lowerName])) { + $lowerName = strtolower(preg_replace('%([a-z])([A-Z])%', '\1-\2', $name)); + if (!isset($this->_headers[$lowerName])) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception("no Header with Name $name found"); + } + } + $name = $lowerName; + + $header = $this->_headers[$name]; + + switch ($format) { + case 'string': + if (is_array($header)) { + $header = implode(Zend_Mime::LINEEND, $header); + } + break; + case 'array': + $header = (array)$header; + default: + // do nothing + } + + return $header; + } + + /** + * Get a specific field from a header like content type or all fields as array + * + * If the header occurs more than once, only the value from the first header + * is returned. + * + * Throws a Zend_Mail_Exception if the requested header does not exist. If + * the specific header field does not exist, returns null. + * + * @param string $name name of header, like in getHeader() + * @param string $wantedPart the wanted part, default is first, if null an array with all parts is returned + * @param string $firstName key name for the first part + * @return string|array wanted part or all parts as array($firstName => firstPart, partname => value) + * @throws Zend_Exception, Zend_Mail_Exception + */ + public function getHeaderField($name, $wantedPart = 0, $firstName = 0) { + return Zend_Mime_Decode::splitHeaderField(current($this->getHeader($name, 'array')), $wantedPart, $firstName); + } + + + /** + * Getter for mail headers - name is matched in lowercase + * + * This getter is short for Zend_Mail_Part::getHeader($name, 'string') + * + * @see Zend_Mail_Part::getHeader() + * + * @param string $name header name + * @return string value of header + * @throws Zend_Mail_Exception + */ + public function __get($name) + { + return $this->getHeader($name, 'string'); + } + + /** + * magic method to get content of part + * + * @return string content + */ + public function __toString() + { + return $this->getContent(); + } + + /** + * implements RecursiveIterator::hasChildren() + * + * @return bool current element has children/is multipart + */ + public function hasChildren() + { + $current = $this->current(); + return $current && $current instanceof Zend_Mail_Part && $current->isMultipart(); + } + + /** + * implements RecursiveIterator::getChildren() + * + * @return Zend_Mail_Part same as self::current() + */ + public function getChildren() + { + return $this->current(); + } + + /** + * implements Iterator::valid() + * + * @return bool check if there's a current element + */ + public function valid() + { + if ($this->_countParts === null) { + $this->countParts(); + } + return $this->_iterationPos && $this->_iterationPos <= $this->_countParts; + } + + /** + * implements Iterator::next() + * + * @return null + */ + public function next() + { + ++$this->_iterationPos; + } + + /** + * implements Iterator::key() + * + * @return string key/number of current part + */ + public function key() + { + return $this->_iterationPos; + } + + /** + * implements Iterator::current() + * + * @return Zend_Mail_Part current part + */ + public function current() + { + return $this->getPart($this->_iterationPos); + } + + /** + * implements Iterator::rewind() + * + * @return null + */ + public function rewind() + { + $this->countParts(); + $this->_iterationPos = 1; + } +} diff --git a/lib/Zend/Mail/Part/File.php b/lib/Zend/Mail/Part/File.php new file mode 100644 index 0000000..6ff5147 --- /dev/null +++ b/lib/Zend/Mail/Part/File.php @@ -0,0 +1,198 @@ +_fh = fopen($params['file'], 'r'); + } else { + $this->_fh = $params['file']; + } + if (!$this->_fh) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('could not open file'); + } + if (isset($params['startPos'])) { + fseek($this->_fh, $params['startPos']); + } + $header = ''; + $endPos = isset($params['endPos']) ? $params['endPos'] : null; + while (($endPos === null || ftell($this->_fh) < $endPos) && trim($line = fgets($this->_fh))) { + $header .= $line; + } + + Zend_Mime_Decode::splitMessage($header, $this->_headers, $null); + + $this->_contentPos[0] = ftell($this->_fh); + if ($endPos !== null) { + $this->_contentPos[1] = $endPos; + } else { + fseek($this->_fh, 0, SEEK_END); + $this->_contentPos[1] = ftell($this->_fh); + } + if (!$this->isMultipart()) { + return; + } + + $boundary = $this->getHeaderField('content-type', 'boundary'); + if (!$boundary) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('no boundary found in content type to split message'); + } + + $part = array(); + $pos = $this->_contentPos[0]; + fseek($this->_fh, $pos); + while (!feof($this->_fh) && ($endPos === null || $pos < $endPos)) { + $line = fgets($this->_fh); + if ($line === false) { + if (feof($this->_fh)) { + break; + } + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('error reading file'); + } + + $lastPos = $pos; + $pos = ftell($this->_fh); + $line = trim($line); + + if ($line == '--' . $boundary) { + if ($part) { + // not first part + $part[1] = $lastPos; + $this->_partPos[] = $part; + } + $part = array($pos); + } else if ($line == '--' . $boundary . '--') { + $part[1] = $lastPos; + $this->_partPos[] = $part; + break; + } + } + $this->_countParts = count($this->_partPos); + + } + + + /** + * Body of part + * + * If part is multipart the raw content of this part with all sub parts is returned + * + * @return string body + * @throws Zend_Mail_Exception + */ + public function getContent($stream = null) + { + fseek($this->_fh, $this->_contentPos[0]); + if ($stream !== null) { + return stream_copy_to_stream($this->_fh, $stream, $this->_contentPos[1] - $this->_contentPos[0]); + } + $length = $this->_contentPos[1] - $this->_contentPos[0]; + return $length < 1 ? '' : fread($this->_fh, $length); + } + + /** + * Return size of part + * + * Quite simple implemented currently (not decoding). Handle with care. + * + * @return int size + */ + public function getSize() { + return $this->_contentPos[1] - $this->_contentPos[0]; + } + + /** + * Get part of multipart message + * + * @param int $num number of part starting with 1 for first part + * @return Zend_Mail_Part wanted part + * @throws Zend_Mail_Exception + */ + public function getPart($num) + { + --$num; + if (!isset($this->_partPos[$num])) { + /** + * @see Zend_Mail_Exception + */ + require_once 'Zend/Mail/Exception.php'; + throw new Zend_Mail_Exception('part not found'); + } + + return new self(array('file' => $this->_fh, 'startPos' => $this->_partPos[$num][0], + 'endPos' => $this->_partPos[$num][1])); + } +} diff --git a/lib/Zend/Mail/Part/Interface.php b/lib/Zend/Mail/Part/Interface.php new file mode 100644 index 0000000..d1a22f1 --- /dev/null +++ b/lib/Zend/Mail/Part/Interface.php @@ -0,0 +1,136 @@ + value) + */ + public function getHeaders(); + + /** + * Get a header in specificed format + * + * Internally headers that occur more than once are saved as array, all other as string. If $format + * is set to string implode is used to concat the values (with Zend_Mime::LINEEND as delim). + * + * @param string $name name of header, matches case-insensitive, but camel-case is replaced with dashes + * @param string $format change type of return value to 'string' or 'array' + * @return string|array value of header in wanted or internal format + * @throws Zend_Mail_Exception + */ + public function getHeader($name, $format = null); + + /** + * Get a specific field from a header like content type or all fields as array + * + * If the header occurs more than once, only the value from the first header + * is returned. + * + * Throws a Zend_Mail_Exception if the requested header does not exist. If + * the specific header field does not exist, returns null. + * + * @param string $name name of header, like in getHeader() + * @param string $wantedPart the wanted part, default is first, if null an array with all parts is returned + * @param string $firstName key name for the first part + * @return string|array wanted part or all parts as array($firstName => firstPart, partname => value) + * @throws Zend_Exception, Zend_Mail_Exception + */ + public function getHeaderField($name, $wantedPart = 0, $firstName = 0); + + + /** + * Getter for mail headers - name is matched in lowercase + * + * This getter is short for Zend_Mail_Part::getHeader($name, 'string') + * + * @see Zend_Mail_Part::getHeader() + * + * @param string $name header name + * @return string value of header + * @throws Zend_Mail_Exception + */ + public function __get($name); + + /** + * magic method to get content of part + * + * @return string content + */ + public function __toString(); +} \ No newline at end of file diff --git a/lib/Zend/Mail/Protocol/Abstract.php b/lib/Zend/Mail/Protocol/Abstract.php new file mode 100644 index 0000000..cfb1f42 --- /dev/null +++ b/lib/Zend/Mail/Protocol/Abstract.php @@ -0,0 +1,385 @@ +_validHost = new Zend_Validate(); + $this->_validHost->addValidator(new Zend_Validate_Hostname(Zend_Validate_Hostname::ALLOW_ALL)); + + if (!$this->_validHost->isValid($host)) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception(join(', ', $this->_validHost->getMessages())); + } + + $this->_host = $host; + $this->_port = $port; + } + + + /** + * Class destructor to cleanup open resources + * + * @return void + */ + public function __destruct() + { + $this->_disconnect(); + } + + + /** + * Create a connection to the remote host + * + * Concrete adapters for this class will implement their own unique connect scripts, using the _connect() method to create the socket resource. + */ + abstract public function connect(); + + + /** + * Retrieve the last client request + * + * @return string + */ + public function getRequest() + { + return $this->_request; + } + + + /** + * Retrieve the last server response + * + * @return array + */ + public function getResponse() + { + return $this->_response; + } + + + /** + * Retrieve the transaction log + * + * @return string + */ + public function getLog() + { + return $this->_log; + } + + + /** + * Reset the transaction log + * + * @return void + */ + public function resetLog() + { + $this->_log = ''; + } + + + /** + * Connect to the server using the supplied transport and target + * + * An example $remote string may be 'tcp://mail.example.com:25' or 'ssh://hostname.com:2222' + * + * @param string $remote Remote + * @throws Zend_Mail_Protocol_Exception + * @return boolean + */ + protected function _connect($remote) + { + $errorNum = 0; + $errorStr = ''; + + // open connection + $this->_socket = @stream_socket_client($remote, $errorNum, $errorStr, self::TIMEOUT_CONNECTION); + + if ($this->_socket === false) { + if ($errorNum == 0) { + $errorStr = 'Could not open socket'; + } + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception($errorStr); + } + + if (($result = stream_set_timeout($this->_socket, self::TIMEOUT_CONNECTION)) === false) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('Could not set stream timeout'); + } + + return $result; + } + + + /** + * Disconnect from remote host and free resource + * + * @return void + */ + protected function _disconnect() + { + if (is_resource($this->_socket)) { + fclose($this->_socket); + } + } + + + /** + * Send the given request followed by a LINEEND to the server. + * + * @param string $request + * @throws Zend_Mail_Protocol_Exception + * @return integer|boolean Number of bytes written to remote host + */ + protected function _send($request) + { + if (!is_resource($this->_socket)) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('No connection has been established to ' . $this->_host); + } + + $this->_request = $request; + + $result = fwrite($this->_socket, $request . self::EOL); + + // Save request to internal log + $this->_log .= $request . self::EOL; + + if ($result === false) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('Could not send request to ' . $this->_host); + } + + return $result; + } + + + /** + * Get a line from the stream. + * + * @var integer $timeout Per-request timeout value if applicable + * @throws Zend_Mail_Protocol_Exception + * @return string + */ + protected function _receive($timeout = null) + { + if (!is_resource($this->_socket)) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('No connection has been established to ' . $this->_host); + } + + // Adapters may wish to supply per-commend timeouts according to appropriate RFC + if ($timeout !== null) { + stream_set_timeout($this->_socket, $timeout); + } + + // Retrieve response + $reponse = fgets($this->_socket, 1024); + + // Save request to internal log + $this->_log .= $reponse; + + // Check meta data to ensure connection is still valid + $info = stream_get_meta_data($this->_socket); + + if (!empty($info['timed_out'])) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception($this->_host . ' has timed out'); + } + + if ($reponse === false) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('Could not read from ' . $this->_host); + } + + return $reponse; + } + + + /** + * Parse server response for successful codes + * + * Read the response from the stream and check for expected return code. + * Throws a Zend_Mail_Protocol_Exception if an unexpected code is returned. + * + * @param string|array $code One or more codes that indicate a successful response + * @throws Zend_Mail_Protocol_Exception + * @return string Last line of response string + */ + protected function _expect($code, $timeout = null) + { + $this->_response = array(); + $cmd = ''; + $msg = ''; + + if (!is_array($code)) { + $code = array($code); + } + + do { + $this->_response[] = $result = $this->_receive($timeout); + sscanf($result, $this->_template, $cmd, $msg); + + if ($cmd === null || !in_array($cmd, $code)) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception($result); + } + + } while (strpos($msg, '-') === 0); // The '-' message prefix indicates an information string instead of a response string. + + return $msg; + } +} diff --git a/lib/Zend/Mail/Protocol/Exception.php b/lib/Zend/Mail/Protocol/Exception.php new file mode 100644 index 0000000..c3add11 --- /dev/null +++ b/lib/Zend/Mail/Protocol/Exception.php @@ -0,0 +1,39 @@ +connect($host, $port, $ssl); + } + } + + /** + * Public destructor + */ + public function __destruct() + { + $this->logout(); + } + + /** + * Open connection to POP3 server + * + * @param string $host hostname of IP address of POP3 server + * @param int|null $port of IMAP server, default is 143 (993 for ssl) + * @param string|bool $ssl use 'SSL', 'TLS' or false + * @return string welcome message + * @throws Zend_Mail_Protocol_Exception + */ + public function connect($host, $port = null, $ssl = false) + { + if ($ssl == 'SSL') { + $host = 'ssl://' . $host; + } + + if ($port === null) { + $port = $ssl === 'SSL' ? 993 : 143; + } + + $errno = 0; + $errstr = ''; + $this->_socket = @fsockopen($host, $port, $errno, $errstr, self::TIMEOUT_CONNECTION); + if (!$this->_socket) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('cannot connect to host : ' . $errno . ' : ' . $errstr); + } + + if (!$this->_assumedNextLine('* OK')) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('host doesn\'t allow connection'); + } + + if ($ssl === 'TLS') { + $result = $this->requestAndResponse('STARTTLS'); + $result = $result && stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + if (!$result) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('cannot enable TLS'); + } + } + } + + /** + * get the next line from socket with error checking, but nothing else + * + * @return string next line + * @throws Zend_Mail_Protocol_Exception + */ + protected function _nextLine() + { + $line = @fgets($this->_socket); + if ($line === false) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('cannot read - connection closed?'); + } + + return $line; + } + + /** + * get next line and assume it starts with $start. some requests give a simple + * feedback so we can quickly check if we can go on. + * + * @param string $start the first bytes we assume to be in the next line + * @return bool line starts with $start + * @throws Zend_Mail_Protocol_Exception + */ + protected function _assumedNextLine($start) + { + $line = $this->_nextLine(); + return strpos($line, $start) === 0; + } + + /** + * get next line and split the tag. that's the normal case for a response line + * + * @param string $tag tag of line is returned by reference + * @return string next line + * @throws Zend_Mail_Protocol_Exception + */ + protected function _nextTaggedLine(&$tag) + { + $line = $this->_nextLine(); + + // seperate tag from line + list($tag, $line) = explode(' ', $line, 2); + + return $line; + } + + /** + * split a given line in tokens. a token is literal of any form or a list + * + * @param string $line line to decode + * @return array tokens, literals are returned as string, lists as array + * @throws Zend_Mail_Protocol_Exception + */ + protected function _decodeLine($line) + { + $tokens = array(); + $stack = array(); + + /* + We start to decode the response here. The unterstood tokens are: + literal + "literal" or also "lit\\er\"al" + {bytes}literal + (literals*) + All tokens are returned in an array. Literals in braces (the last unterstood + token in the list) are returned as an array of tokens. I.e. the following response: + "foo" baz {3}bar ("f\\\"oo" bar) + would be returned as: + array('foo', 'baz', 'bar', array('f\\\"oo', 'bar')); + + // TODO: add handling of '[' and ']' to parser for easier handling of response text + */ + // replace any trailling including spaces with a single space + $line = rtrim($line) . ' '; + while (($pos = strpos($line, ' ')) !== false) { + $token = substr($line, 0, $pos); + while ($token[0] == '(') { + array_push($stack, $tokens); + $tokens = array(); + $token = substr($token, 1); + } + if ($token[0] == '"') { + if (preg_match('%^"((.|\\\\|\\")*?)" *%', $line, $matches)) { + $tokens[] = $matches[1]; + $line = substr($line, strlen($matches[0])); + continue; + } + } + if ($token[0] == '{') { + $endPos = strpos($token, '}'); + $chars = substr($token, 1, $endPos - 1); + if (is_numeric($chars)) { + $token = ''; + while (strlen($token) < $chars) { + $token .= $this->_nextLine(); + } + $line = ''; + if (strlen($token) > $chars) { + $line = substr($token, $chars); + $token = substr($token, 0, $chars); + } else { + $line .= $this->_nextLine(); + } + $tokens[] = $token; + $line = trim($line) . ' '; + continue; + } + } + if ($stack && $token[strlen($token) - 1] == ')') { + // closing braces are not seperated by spaces, so we need to count them + $braces = strlen($token); + $token = rtrim($token, ')'); + // only count braces if more than one + $braces -= strlen($token) + 1; + // only add if token had more than just closing braces + if ($token) { + $tokens[] = $token; + } + $token = $tokens; + $tokens = array_pop($stack); + // special handline if more than one closing brace + while ($braces-- > 0) { + $tokens[] = $token; + $token = $tokens; + $tokens = array_pop($stack); + } + } + $tokens[] = $token; + $line = substr($line, $pos + 1); + } + + // maybe the server forgot to send some closing braces + while ($stack) { + $child = $tokens; + $tokens = array_pop($stack); + $tokens[] = $child; + } + + return $tokens; + } + + /** + * read a response "line" (could also be more than one real line if response has {..}) + * and do a simple decode + * + * @param array|string $tokens decoded tokens are returned by reference, if $dontParse + * is true the unparsed line is returned here + * @param string $wantedTag check for this tag for response code. Default '*' is + * continuation tag. + * @param bool $dontParse if true only the unparsed line is returned $tokens + * @return bool if returned tag matches wanted tag + * @throws Zend_Mail_Protocol_Exception + */ + public function readLine(&$tokens = array(), $wantedTag = '*', $dontParse = false) + { + $line = $this->_nextTaggedLine($tag); + if (!$dontParse) { + $tokens = $this->_decodeLine($line); + } else { + $tokens = $line; + } + + // if tag is wanted tag we might be at the end of a multiline response + return $tag == $wantedTag; + } + + /** + * read all lines of response until given tag is found (last line of response) + * + * @param string $tag the tag of your request + * @param string|array $filter you can filter the response so you get only the + * given response lines + * @param bool $dontParse if true every line is returned unparsed instead of + * the decoded tokens + * @return null|bool|array tokens if success, false if error, null if bad request + * @throws Zend_Mail_Protocol_Exception + */ + public function readResponse($tag, $dontParse = false) + { + $lines = array(); + while (!$this->readLine($tokens, $tag, $dontParse)) { + $lines[] = $tokens; + } + + if ($dontParse) { + // last to chars are still needed for response code + $tokens = array(substr($tokens, 0, 2)); + } + // last line has response code + if ($tokens[0] == 'OK') { + return $lines ? $lines : true; + } else if ($tokens[0] == 'NO'){ + return false; + } + return null; + } + + /** + * send a request + * + * @param string $command your request command + * @param array $tokens additional parameters to command, use escapeString() to prepare + * @param string $tag provide a tag otherwise an autogenerated is returned + * @return null + * @throws Zend_Mail_Protocol_Exception + */ + public function sendRequest($command, $tokens = array(), &$tag = null) + { + if (!$tag) { + ++$this->_tagCount; + $tag = 'TAG' . $this->_tagCount; + } + + $line = $tag . ' ' . $command; + + foreach ($tokens as $token) { + if (is_array($token)) { + if (@fputs($this->_socket, $line . ' ' . $token[0] . "\r\n") === false) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('cannot write - connection closed?'); + } + if (!$this->_assumedNextLine('+ ')) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('cannot send literal string'); + } + $line = $token[1]; + } else { + $line .= ' ' . $token; + } + } + + if (@fputs($this->_socket, $line . "\r\n") === false) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('cannot write - connection closed?'); + } + } + + /** + * send a request and get response at once + * + * @param string $command command as in sendRequest() + * @param array $tokens parameters as in sendRequest() + * @param bool $dontParse if true unparsed lines are returned instead of tokens + * @return mixed response as in readResponse() + * @throws Zend_Mail_Protocol_Exception + */ + public function requestAndResponse($command, $tokens = array(), $dontParse = false) + { + $this->sendRequest($command, $tokens, $tag); + $response = $this->readResponse($tag, $dontParse); + + return $response; + } + + /** + * escape one or more literals i.e. for sendRequest + * + * @param string|array $string the literal/-s + * @return string|array escape literals, literals with newline ar returned + * as array('{size}', 'string'); + */ + public function escapeString($string) + { + if (func_num_args() < 2) { + if (strpos($string, "\n") !== false) { + return array('{' . strlen($string) . '}', $string); + } else { + return '"' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $string) . '"'; + } + } + $result = array(); + foreach (func_get_args() as $string) { + $result[] = $this->escapeString($string); + } + return $result; + } + + /** + * escape a list with literals or lists + * + * @param array $list list with literals or lists as PHP array + * @return string escaped list for imap + */ + public function escapeList($list) + { + $result = array(); + foreach ($list as $k => $v) { + if (!is_array($v)) { +// $result[] = $this->escapeString($v); + $result[] = $v; + continue; + } + $result[] = $this->escapeList($v); + } + return '(' . implode(' ', $result) . ')'; + } + + /** + * Login to IMAP server. + * + * @param string $user username + * @param string $password password + * @return bool success + * @throws Zend_Mail_Protocol_Exception + */ + public function login($user, $password) + { + return $this->requestAndResponse('LOGIN', $this->escapeString($user, $password), true); + } + + /** + * logout of imap server + * + * @return bool success + */ + public function logout() + { + $result = false; + if ($this->_socket) { + try { + $result = $this->requestAndResponse('LOGOUT', array(), true); + } catch (Zend_Mail_Protocol_Exception $e) { + // ignoring exception + } + fclose($this->_socket); + $this->_socket = null; + } + return $result; + } + + + /** + * Get capabilities from IMAP server + * + * @return array list of capabilities + * @throws Zend_Mail_Protocol_Exception + */ + public function capability() + { + $response = $this->requestAndResponse('CAPABILITY'); + + if (!$response) { + return $response; + } + + $capabilities = array(); + foreach ($response as $line) { + $capabilities = array_merge($capabilities, $line); + } + return $capabilities; + } + + /** + * Examine and select have the same response. The common code for both + * is in this method + * + * @param string $command can be 'EXAMINE' or 'SELECT' and this is used as command + * @param string $box which folder to change to or examine + * @return bool|array false if error, array with returned information + * otherwise (flags, exists, recent, uidvalidity) + * @throws Zend_Mail_Protocol_Exception + */ + public function examineOrSelect($command = 'EXAMINE', $box = 'INBOX') + { + $this->sendRequest($command, array($this->escapeString($box)), $tag); + + $result = array(); + while (!$this->readLine($tokens, $tag)) { + if ($tokens[0] == 'FLAGS') { + array_shift($tokens); + $result['flags'] = $tokens; + continue; + } + switch ($tokens[1]) { + case 'EXISTS': + case 'RECENT': + $result[strtolower($tokens[1])] = $tokens[0]; + break; + case '[UIDVALIDITY': + $result['uidvalidity'] = (int)$tokens[2]; + break; + default: + // ignore + } + } + + if ($tokens[0] != 'OK') { + return false; + } + return $result; + } + + /** + * change folder + * + * @param string $box change to this folder + * @return bool|array see examineOrselect() + * @throws Zend_Mail_Protocol_Exception + */ + public function select($box = 'INBOX') + { + return $this->examineOrSelect('SELECT', $box); + } + + /** + * examine folder + * + * @param string $box examine this folder + * @return bool|array see examineOrselect() + * @throws Zend_Mail_Protocol_Exception + */ + public function examine($box = 'INBOX') + { + return $this->examineOrSelect('EXAMINE', $box); + } + + /** + * fetch one or more items of one or more messages + * + * @param string|array $items items to fetch from message(s) as string (if only one item) + * or array of strings + * @param int $from message for items or start message if $to !== null + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message avaible + * @return string|array if only one item of one message is fetched it's returned as string + * if items of one message are fetched it's returned as (name => value) + * if one items of messages are fetched it's returned as (msgno => value) + * if items of messages are fetchted it's returned as (msgno => (name => value)) + * @throws Zend_Mail_Protocol_Exception + */ + public function fetch($items, $from, $to = null) + { + if (is_array($from)) { + $set = implode(',', $from); + } else if ($to === null) { + $set = (int)$from; + } else if ($to === INF) { + $set = (int)$from . ':*'; + } else { + $set = (int)$from . ':' . (int)$to; + } + + $items = (array)$items; + $itemList = $this->escapeList($items); + + $this->sendRequest('FETCH', array($set, $itemList), $tag); + + $result = array(); + while (!$this->readLine($tokens, $tag)) { + // ignore other responses + if ($tokens[1] != 'FETCH') { + continue; + } + // ignore other messages + if ($to === null && !is_array($from) && $tokens[0] != $from) { + continue; + } + // if we only want one item we return that one directly + if (count($items) == 1) { + if ($tokens[2][0] == $items[0]) { + $data = $tokens[2][1]; + } else { + // maybe the server send an other field we didn't wanted + $count = count($tokens[2]); + // we start with 2, because 0 was already checked + for ($i = 2; $i < $count; $i += 2) { + if ($tokens[2][$i] != $items[0]) { + continue; + } + $data = $tokens[2][$i + 1]; + break; + } + } + } else { + $data = array(); + while (key($tokens[2]) !== null) { + $data[current($tokens[2])] = next($tokens[2]); + next($tokens[2]); + } + } + // if we want only one message we can ignore everything else and just return + if ($to === null && !is_array($from) && $tokens[0] == $from) { + // we still need to read all lines + while (!$this->readLine($tokens, $tag)); + return $data; + } + $result[$tokens[0]] = $data; + } + + if ($to === null && !is_array($from)) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('the single id was not found in response'); + } + + return $result; + } + + /** + * get mailbox list + * + * this method can't be named after the IMAP command 'LIST', as list is a reserved keyword + * + * @param string $reference mailbox reference for list + * @param string $mailbox mailbox name match with wildcards + * @return array mailboxes that matched $mailbox as array(globalName => array('delim' => .., 'flags' => ..)) + * @throws Zend_Mail_Protocol_Exception + */ + public function listMailbox($reference = '', $mailbox = '*') + { + $result = array(); + $list = $this->requestAndResponse('LIST', $this->escapeString($reference, $mailbox)); + if (!$list || $list === true) { + return $result; + } + + foreach ($list as $item) { + if (count($item) != 4 || $item[0] != 'LIST') { + continue; + } + $result[$item[3]] = array('delim' => $item[2], 'flags' => $item[1]); + } + + return $result; + } + + /** + * set flags + * + * @param array $flags flags to set, add or remove - see $mode + * @param int $from message for items or start message if $to !== null + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message avaible + * @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given + * @param bool $silent if false the return values are the new flags for the wanted messages + * @return bool|array new flags if $silent is false, else true or false depending on success + * @throws Zend_Mail_Protocol_Exception + */ + public function store(array $flags, $from, $to = null, $mode = null, $silent = true) + { + $item = 'FLAGS'; + if ($mode == '+' || $mode == '-') { + $item = $mode . $item; + } + if ($silent) { + $item .= '.SILENT'; + } + + $flags = $this->escapeList($flags); + $set = (int)$from; + if ($to != null) { + $set .= ':' . ($to == INF ? '*' : (int)$to); + } + + $result = $this->requestAndResponse('STORE', array($set, $item, $flags), $silent); + + if ($silent) { + return $result ? true : false; + } + + $tokens = $result; + $result = array(); + foreach ($tokens as $token) { + if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') { + continue; + } + $result[$token[0]] = $token[2][1]; + } + + return $result; + } + + /** + * append a new message to given folder + * + * @param string $folder name of target folder + * @param string $message full message content + * @param array $flags flags for new message + * @param string $date date for new message + * @return bool success + * @throws Zend_Mail_Protocol_Exception + */ + public function append($folder, $message, $flags = null, $date = null) + { + $tokens = array(); + $tokens[] = $this->escapeString($folder); + if ($flags !== null) { + $tokens[] = $this->escapeList($flags); + } + if ($date !== null) { + $tokens[] = $this->escapeString($date); + } + $tokens[] = $this->escapeString($message); + + return $this->requestAndResponse('APPEND', $tokens, true); + } + + /** + * copy message set from current folder to other folder + * + * @param string $folder destination folder + * @param int|null $to if null only one message ($from) is fetched, else it's the + * last message, INF means last message avaible + * @return bool success + * @throws Zend_Mail_Protocol_Exception + */ + public function copy($folder, $from, $to = null) + { + $set = (int)$from; + if ($to != null) { + $set .= ':' . ($to == INF ? '*' : (int)$to); + } + + return $this->requestAndResponse('COPY', array($set, $this->escapeString($folder)), true); + } + + /** + * create a new folder (and parent folders if needed) + * + * @param string $folder folder name + * @return bool success + */ + public function create($folder) + { + return $this->requestAndResponse('CREATE', array($this->escapeString($folder)), true); + } + + /** + * rename an existing folder + * + * @param string $old old name + * @param string $new new name + * @return bool success + */ + public function rename($old, $new) + { + return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); + } + + /** + * remove a folder + * + * @param string $folder folder name + * @return bool success + */ + public function delete($folder) + { + return $this->requestAndResponse('DELETE', array($this->escapeString($folder)), true); + } + + /** + * permanently remove messages + * + * @return bool success + */ + public function expunge() + { + // TODO: parse response? + return $this->requestAndResponse('EXPUNGE'); + } + + /** + * send noop + * + * @return bool success + */ + public function noop() + { + // TODO: parse response + return $this->requestAndResponse('NOOP'); + } + + /** + * do a search request + * + * This method is currently marked as internal as the API might change and is not + * safe if you don't take precautions. + * + * @internal + * @return array message ids + */ + public function search(array $params) + { + $response = $this->requestAndResponse('SEARCH', $params); + if (!$response) { + return $response; + } + + foreach ($response as $ids) { + if ($ids[0] == 'SEARCH') { + array_shift($ids); + return $ids; + } + } + return array(); + } + +} diff --git a/lib/Zend/Mail/Protocol/Pop3.php b/lib/Zend/Mail/Protocol/Pop3.php new file mode 100644 index 0000000..c1f968a --- /dev/null +++ b/lib/Zend/Mail/Protocol/Pop3.php @@ -0,0 +1,471 @@ +connect($host, $port, $ssl); + } + } + + + /** + * Public destructor + */ + public function __destruct() + { + $this->logout(); + } + + + /** + * Open connection to POP3 server + * + * @param string $host hostname of IP address of POP3 server + * @param int|null $port of POP3 server, default is 110 (995 for ssl) + * @param string|bool $ssl use 'SSL', 'TLS' or false + * @return string welcome message + * @throws Zend_Mail_Protocol_Exception + */ + public function connect($host, $port = null, $ssl = false) + { + if ($ssl == 'SSL') { + $host = 'ssl://' . $host; + } + + if ($port === null) { + $port = $ssl == 'SSL' ? 995 : 110; + } + + $errno = 0; + $errstr = ''; + $this->_socket = @fsockopen($host, $port, $errno, $errstr, self::TIMEOUT_CONNECTION); + if (!$this->_socket) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('cannot connect to host : ' . $errno . ' : ' . $errstr); + } + + $welcome = $this->readResponse(); + + strtok($welcome, '<'); + $this->_timestamp = strtok('>'); + if (!strpos($this->_timestamp, '@')) { + $this->_timestamp = null; + } else { + $this->_timestamp = '<' . $this->_timestamp . '>'; + } + + if ($ssl === 'TLS') { + $this->request('STLS'); + $result = stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + if (!$result) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('cannot enable TLS'); + } + } + + return $welcome; + } + + + /** + * Send a request + * + * @param string $request your request without newline + * @return null + * @throws Zend_Mail_Protocol_Exception + */ + public function sendRequest($request) + { + $result = @fputs($this->_socket, $request . "\r\n"); + if (!$result) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('send failed - connection closed?'); + } + } + + + /** + * read a response + * + * @param boolean $multiline response has multiple lines and should be read until "." + * @return string response + * @throws Zend_Mail_Protocol_Exception + */ + public function readResponse($multiline = false) + { + $result = @fgets($this->_socket); + if (!is_string($result)) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('read failed - connection closed?'); + } + + $result = trim($result); + if (strpos($result, ' ')) { + list($status, $message) = explode(' ', $result, 2); + } else { + $status = $result; + $message = ''; + } + + if ($status != '+OK') { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('last request failed'); + } + + if ($multiline) { + $message = ''; + $line = fgets($this->_socket); + while ($line && rtrim($line, "\r\n") != '.') { + if ($line[0] == '.') { + $line = substr($line, 1); + } + $message .= $line; + $line = fgets($this->_socket); + }; + } + + return $message; + } + + + /** + * Send request and get resposne + * + * @see sendRequest(), readResponse() + * + * @param string $request request + * @param bool $multiline multiline response? + * @return string result from readResponse() + * @throws Zend_Mail_Protocol_Exception + */ + public function request($request, $multiline = false) + { + $this->sendRequest($request); + return $this->readResponse($multiline); + } + + + /** + * End communication with POP3 server (also closes socket) + * + * @return null + */ + public function logout() + { + if (!$this->_socket) { + return; + } + + try { + $this->request('QUIT'); + } catch (Zend_Mail_Protocol_Exception $e) { + // ignore error - we're closing the socket anyway + } + + fclose($this->_socket); + $this->_socket = null; + } + + + /** + * Get capabilities from POP3 server + * + * @return array list of capabilities + * @throws Zend_Mail_Protocol_Exception + */ + public function capa() + { + $result = $this->request('CAPA', true); + return explode("\n", $result); + } + + + /** + * Login to POP3 server. Can use APOP + * + * @param string $user username + * @param string $password password + * @param bool $try_apop should APOP be tried? + * @return void + * @throws Zend_Mail_Protocol_Exception + */ + public function login($user, $password, $tryApop = true) + { + if ($tryApop && $this->_timestamp) { + try { + $this->request("APOP $user " . md5($this->_timestamp . $password)); + return; + } catch (Zend_Mail_Protocol_Exception $e) { + // ignore + } + } + + $result = $this->request("USER $user"); + $result = $this->request("PASS $password"); + } + + + /** + * Make STAT call for message count and size sum + * + * @param int $messages out parameter with count of messages + * @param int $octets out parameter with size in octects of messages + * @return void + * @throws Zend_Mail_Protocol_Exception + */ + public function status(&$messages, &$octets) + { + $messages = 0; + $octets = 0; + $result = $this->request('STAT'); + + list($messages, $octets) = explode(' ', $result); + } + + + /** + * Make LIST call for size of message(s) + * + * @param int|null $msgno number of message, null for all + * @return int|array size of given message or list with array(num => size) + * @throws Zend_Mail_Protocol_Exception + */ + public function getList($msgno = null) + { + if ($msgno !== null) { + $result = $this->request("LIST $msgno"); + + list(, $result) = explode(' ', $result); + return (int)$result; + } + + $result = $this->request('LIST', true); + $messages = array(); + $line = strtok($result, "\n"); + while ($line) { + list($no, $size) = explode(' ', trim($line)); + $messages[(int)$no] = (int)$size; + $line = strtok("\n"); + } + + return $messages; + } + + + /** + * Make UIDL call for getting a uniqueid + * + * @param int|null $msgno number of message, null for all + * @return string|array uniqueid of message or list with array(num => uniqueid) + * @throws Zend_Mail_Protocol_Exception + */ + public function uniqueid($msgno = null) + { + if ($msgno !== null) { + $result = $this->request("UIDL $msgno"); + + list(, $result) = explode(' ', $result); + return $result; + } + + $result = $this->request('UIDL', true); + + $result = explode("\n", $result); + $messages = array(); + foreach ($result as $line) { + if (!$line) { + continue; + } + list($no, $id) = explode(' ', trim($line), 2); + $messages[(int)$no] = $id; + } + + return $messages; + + } + + + /** + * Make TOP call for getting headers and maybe some body lines + * This method also sets hasTop - before it it's not known if top is supported + * + * The fallback makes normale RETR call, which retrieves the whole message. Additional + * lines are not removed. + * + * @param int $msgno number of message + * @param int $lines number of wanted body lines (empty line is inserted after header lines) + * @param bool $fallback fallback with full retrieve if top is not supported + * @return string message headers with wanted body lines + * @throws Zend_Mail_Protocol_Exception + */ + public function top($msgno, $lines = 0, $fallback = false) + { + if ($this->hasTop === false) { + if ($fallback) { + return $this->retrieve($msgno); + } else { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('top not supported and no fallback wanted'); + } + } + $this->hasTop = true; + + $lines = (!$lines || $lines < 1) ? 0 : (int)$lines; + + try { + $result = $this->request("TOP $msgno $lines", true); + } catch (Zend_Mail_Protocol_Exception $e) { + $this->hasTop = false; + if ($fallback) { + $result = $this->retrieve($msgno); + } else { + throw $e; + } + } + + return $result; + } + + + /** + * Make a RETR call for retrieving a full message with headers and body + * + * @deprecated since 1.1.0; this method has a typo - please use retrieve() + * @param int $msgno message number + * @return string message + * @throws Zend_Mail_Protocol_Exception + */ + public function retrive($msgno) + { + return $this->retrieve($msgno); + } + + + /** + * Make a RETR call for retrieving a full message with headers and body + * + * @param int $msgno message number + * @return string message + * @throws Zend_Mail_Protocol_Exception + */ + public function retrieve($msgno) + { + $result = $this->request("RETR $msgno", true); + return $result; + } + + /** + * Make a NOOP call, maybe needed for keeping the server happy + * + * @return null + * @throws Zend_Mail_Protocol_Exception + */ + public function noop() + { + $this->request('NOOP'); + } + + + /** + * Make a DELE count to remove a message + * + * @return null + * @throws Zend_Mail_Protocol_Exception + */ + public function delete($msgno) + { + $this->request("DELE $msgno"); + } + + + /** + * Make RSET call, which rollbacks delete requests + * + * @return null + * @throws Zend_Mail_Protocol_Exception + */ + public function undelete() + { + $this->request('RSET'); + } +} diff --git a/lib/Zend/Mail/Protocol/Smtp.php b/lib/Zend/Mail/Protocol/Smtp.php new file mode 100644 index 0000000..18ac77a --- /dev/null +++ b/lib/Zend/Mail/Protocol/Smtp.php @@ -0,0 +1,443 @@ +_secure = 'tls'; + break; + + case 'ssl': + $this->_transport = 'ssl'; + $this->_secure = 'ssl'; + if ($port == null) { + $port = 465; + } + break; + + default: + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception($config['ssl'] . ' is unsupported SSL type'); + break; + } + } + + // If no port has been specified then check the master PHP ini file. Defaults to 25 if the ini setting is null. + if ($port == null) { + if (($port = ini_get('smtp_port')) == '') { + $port = 25; + } + } + + parent::__construct($host, $port); + } + + + /** + * Connect to the server with the parameters given in the constructor. + * + * @return boolean + */ + public function connect() + { + return $this->_connect($this->_transport . '://' . $this->_host . ':'. $this->_port); + } + + + /** + * Initiate HELO/EHLO sequence and set flag to indicate valid smtp session + * + * @param string $host The client hostname or IP address (default: 127.0.0.1) + * @throws Zend_Mail_Protocol_Exception + * @return void + */ + public function helo($host = '127.0.0.1') + { + // Respect RFC 2821 and disallow HELO attempts if session is already initiated. + if ($this->_sess === true) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('Cannot issue HELO to existing session'); + } + + // Validate client hostname + if (!$this->_validHost->isValid($host)) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception(join(', ', $this->_validHost->getMessages())); + } + + // Initiate helo sequence + $this->_expect(220, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + $this->_ehlo($host); + + // If a TLS session is required, commence negotiation + if ($this->_secure == 'tls') { + $this->_send('STARTTLS'); + $this->_expect(220, 180); + if (!stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('Unable to connect via TLS'); + } + $this->_ehlo($host); + } + + $this->_startSession(); + $this->auth(); + } + + + /** + * Send EHLO or HELO depending on capabilities of smtp host + * + * @param string $host The client hostname or IP address (default: 127.0.0.1) + * @throws Zend_Mail_Protocol_Exception + * @return void + */ + protected function _ehlo($host) + { + // Support for older, less-compliant remote servers. Tries multiple attempts of EHLO or HELO. + try { + $this->_send('EHLO ' . $host); + $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + } catch (Zend_Mail_Protocol_Exception $e) { + $this->_send('HELO ' . $host); + $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + } catch (Zend_Mail_Protocol_Exception $e) { + throw $e; + } + } + + + /** + * Issues MAIL command + * + * @param string $from Sender mailbox + * @throws Zend_Mail_Protocol_Exception + * @return void + */ + public function mail($from) + { + if ($this->_sess !== true) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('A valid session has not been started'); + } + + $this->_send('MAIL FROM:<' . $from . '>'); + $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + + // Set mail to true, clear recipients and any existing data flags as per 4.1.1.2 of RFC 2821 + $this->_mail = true; + $this->_rcpt = false; + $this->_data = false; + } + + + /** + * Issues RCPT command + * + * @param string $to Receiver(s) mailbox + * @throws Zend_Mail_Protocol_Exception + * @return void + */ + public function rcpt($to) + { + if ($this->_mail !== true) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('No sender reverse path has been supplied'); + } + + // Set rcpt to true, as per 4.1.1.3 of RFC 2821 + $this->_send('RCPT TO:<' . $to . '>'); + $this->_expect(array(250, 251), 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + $this->_rcpt = true; + } + + + /** + * Issues DATA command + * + * @param string $data + * @throws Zend_Mail_Protocol_Exception + * @return void + */ + public function data($data) + { + // Ensure recipients have been set + if ($this->_rcpt !== true) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('No recipient forward path has been supplied'); + } + + $this->_send('DATA'); + $this->_expect(354, 120); // Timeout set for 2 minutes as per RFC 2821 4.5.3.2 + + foreach (explode(Zend_Mime::LINEEND, $data) as $line) { + if (strpos($line, '.') === 0) { + // Escape lines prefixed with a '.' + $line = '.' . $line; + } + $this->_send($line); + } + + $this->_send('.'); + $this->_expect(250, 600); // Timeout set for 10 minutes as per RFC 2821 4.5.3.2 + $this->_data = true; + } + + + /** + * Issues the RSET command end validates answer + * + * Can be used to restore a clean smtp communication state when a transaction has been cancelled or commencing a new transaction. + * + * @return void + */ + public function rset() + { + $this->_send('RSET'); + // MS ESMTP doesn't follow RFC, see [ZF-1377] + $this->_expect(array(250, 220)); + + $this->_mail = false; + $this->_rcpt = false; + $this->_data = false; + } + + + /** + * Issues the NOOP command end validates answer + * + * Not used by Zend_Mail, could be used to keep a connection alive or check if it is still open. + * + * @return void + */ + public function noop() + { + $this->_send('NOOP'); + $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + } + + + /** + * Issues the VRFY command end validates answer + * + * Not used by Zend_Mail. + * + * @param string $user User Name or eMail to verify + * @return void + */ + public function vrfy($user) + { + $this->_send('VRFY ' . $user); + $this->_expect(array(250, 251, 252), 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + } + + + /** + * Issues the QUIT command and clears the current session + * + * @return void + */ + public function quit() + { + if ($this->_sess) { + $this->_send('QUIT'); + $this->_expect(221, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + $this->_stopSession(); + } + } + + + /** + * Default authentication method + * + * This default method is implemented by AUTH adapters to properly authenticate to a remote host. + * + * @throws Zend_Mail_Protocol_Exception + * @return void + */ + public function auth() + { + if ($this->_auth === true) { + /** + * @see Zend_Mail_Protocol_Exception + */ + require_once 'Zend/Mail/Protocol/Exception.php'; + throw new Zend_Mail_Protocol_Exception('Already authenticated for this session'); + } + } + + + /** + * Closes connection + * + * @return void + */ + public function disconnect() + { + $this->_disconnect(); + } + + + /** + * Start mail session + * + * @return void + */ + protected function _startSession() + { + $this->_sess = true; + } + + + /** + * Stop mail session + * + * @return void + */ + protected function _stopSession() + { + $this->_sess = false; + } +} diff --git a/lib/Zend/Mail/Protocol/Smtp/Auth/Crammd5.php b/lib/Zend/Mail/Protocol/Smtp/Auth/Crammd5.php new file mode 100644 index 0000000..337c7eb --- /dev/null +++ b/lib/Zend/Mail/Protocol/Smtp/Auth/Crammd5.php @@ -0,0 +1,108 @@ +_username = $config['username']; + } + if (isset($config['password'])) { + $this->_password = $config['password']; + } + } + + parent::__construct($host, $port, $config); + } + + + /** + * @todo Perform CRAM-MD5 authentication with supplied credentials + * + * @return void + */ + public function auth() + { + // Ensure AUTH has not already been initiated. + parent::auth(); + + $this->_send('AUTH CRAM-MD5'); + $challenge = $this->_expect(334); + $challenge = base64_decode($challenge); + $digest = $this->_hmacMd5($this->_password, $challenge); + $this->_send(base64_encode($this->_username . ' ' . $digest)); + $this->_expect(235); + $this->_auth = true; + } + + + /** + * Prepare CRAM-MD5 response to server's ticket + * + * @param string $key Challenge key (usually password) + * @param string $data Challenge data + * @param string $block Length of blocks + * @return string + */ + protected function _hmacMd5($key, $data, $block = 64) + { + if (strlen($key) > 64) { + $key = pack('H32', md5($key)); + } elseif (strlen($key) < 64) { + $key = str_pad($key, $block, chr(0)); + } + + $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64); + $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64); + + $inner = pack('H32', md5($k_ipad . $data)); + $digest = md5($k_opad . $inner); + + return $digest; + } +} diff --git a/lib/Zend/Mail/Protocol/Smtp/Auth/Login.php b/lib/Zend/Mail/Protocol/Smtp/Auth/Login.php new file mode 100644 index 0000000..6332207 --- /dev/null +++ b/lib/Zend/Mail/Protocol/Smtp/Auth/Login.php @@ -0,0 +1,98 @@ +_username = $config['username']; + } + if (isset($config['password'])) { + $this->_password = $config['password']; + } + } + + parent::__construct($host, $port, $config); + } + + + /** + * Perform LOGIN authentication with supplied credentials + * + * @return void + */ + public function auth() + { + // Ensure AUTH has not already been initiated. + parent::auth(); + + $this->_send('AUTH LOGIN'); + $this->_expect(334); + $this->_send(base64_encode($this->_username)); + $this->_expect(334); + $this->_send(base64_encode($this->_password)); + $this->_expect(235); + $this->_auth = true; + } +} diff --git a/lib/Zend/Mail/Protocol/Smtp/Auth/Plain.php b/lib/Zend/Mail/Protocol/Smtp/Auth/Plain.php new file mode 100644 index 0000000..cb8d019 --- /dev/null +++ b/lib/Zend/Mail/Protocol/Smtp/Auth/Plain.php @@ -0,0 +1,96 @@ +_username = $config['username']; + } + if (isset($config['password'])) { + $this->_password = $config['password']; + } + } + + parent::__construct($host, $port, $config); + } + + + /** + * Perform PLAIN authentication with supplied credentials + * + * @return void + */ + public function auth() + { + // Ensure AUTH has not already been initiated. + parent::auth(); + + $this->_send('AUTH PLAIN'); + $this->_expect(334); + $this->_send(base64_encode(chr(0) . $this->_username . chr(0) . $this->_password)); + $this->_expect(235); + $this->_auth = true; + } +} diff --git a/lib/Zend/Mail/Storage.php b/lib/Zend/Mail/Storage.php new file mode 100644 index 0000000..227f496 --- /dev/null +++ b/lib/Zend/Mail/Storage.php @@ -0,0 +1,39 @@ + true, + 'delete' => false, + 'create' => false, + 'top' => false, + 'fetchPart' => true, + 'flags' => false); + + /** + * current iteration position + * @var int + */ + protected $_iterationPos = 0; + + /** + * maximum iteration position (= message count) + * @var null|int + */ + protected $_iterationMax = null; + + /** + * used message class, change it in an extened class to extend the returned message class + * @var string + */ + protected $_messageClass = 'Zend_Mail_Message'; + + /** + * Getter for has-properties. The standard has properties + * are: hasFolder, hasUniqueid, hasDelete, hasCreate, hasTop + * + * The valid values for the has-properties are: + * - true if a feature is supported + * - false if a feature is not supported + * - null is it's not yet known or it can't be know if a feature is supported + * + * @param string $var property name + * @return bool supported or not + * @throws Zend_Mail_Storage_Exception + */ + public function __get($var) + { + if (strpos($var, 'has') === 0) { + $var = strtolower(substr($var, 3)); + return isset($this->_has[$var]) ? $this->_has[$var] : null; + } + + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception($var . ' not found'); + } + + + /** + * Get a full list of features supported by the specific mail lib and the server + * + * @return array list of features as array(featurename => true|false[|null]) + */ + public function getCapabilities() + { + return $this->_has; + } + + + /** + * Count messages messages in current box/folder + * + * @return int number of messages + * @throws Zend_Mail_Storage_Exception + */ + abstract public function countMessages(); + + + /** + * Get a list of messages with number and size + * + * @param int $id number of message + * @return int|array size of given message of list with all messages as array(num => size) + */ + abstract public function getSize($id = 0); + + + /** + * Get a message with headers and body + * + * @param $id int number of message + * @return Zend_Mail_Message + */ + abstract public function getMessage($id); + + + /** + * Get raw header of message or part + * + * @param int $id number of message + * @param null|array|string $part path to part or null for messsage header + * @param int $topLines include this many lines with header (after an empty line) + * @return string raw header + */ + abstract public function getRawHeader($id, $part = null, $topLines = 0); + + /** + * Get raw content of message or part + * + * @param int $id number of message + * @param null|array|string $part path to part or null for messsage content + * @return string raw content + */ + abstract public function getRawContent($id, $part = null); + + /** + * Create instance with parameters + * + * @param array $params mail reader specific parameters + * @throws Zend_Mail_Storage_Exception + */ + abstract public function __construct($params); + + + /** + * Destructor calls close() and therefore closes the resource. + */ + public function __destruct() + { + $this->close(); + } + + + /** + * Close resource for mail lib. If you need to control, when the resource + * is closed. Otherwise the destructor would call this. + * + * @return null + */ + abstract public function close(); + + + /** + * Keep the resource alive. + * + * @return null + */ + abstract public function noop(); + + /** + * delete a message from current box/folder + * + * @return null + */ + abstract public function removeMessage($id); + + /** + * get unique id for one or all messages + * + * if storage does not support unique ids it's the same as the message number + * + * @param int|null $id message number + * @return array|string message number for given message or all messages as array + * @throws Zend_Mail_Storage_Exception + */ + abstract public function getUniqueId($id = null); + + /** + * get a message number from a unique id + * + * I.e. if you have a webmailer that supports deleting messages you should use unique ids + * as parameter and use this method to translate it to message number right before calling removeMessage() + * + * @param string $id unique id + * @return int message number + * @throws Zend_Mail_Storage_Exception + */ + abstract public function getNumberByUniqueId($id); + + // interface implementations follows + + /** + * Countable::count() + * + * @return int + */ + public function count() + { + return $this->countMessages(); + } + + + /** + * ArrayAccess::offsetExists() + * + * @param int $id + * @return boolean + */ + public function offsetExists($id) + { + try { + if ($this->getMessage($id)) { + return true; + } + } catch(Zend_Mail_Storage_Exception $e) {} + + return false; + } + + + /** + * ArrayAccess::offsetGet() + * + * @param int $id + * @return Zend_Mail_Message message object + */ + public function offsetGet($id) + { + return $this->getMessage($id); + } + + + /** + * ArrayAccess::offsetSet() + * + * @param id $id + * @param mixed $value + * @throws Zend_Mail_Storage_Exception + * @return void + */ + public function offsetSet($id, $value) + { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot write mail messages via array access'); + } + + + /** + * ArrayAccess::offsetUnset() + * + * @param int $id + * @return boolean success + */ + public function offsetUnset($id) + { + return $this->removeMessage($id); + } + + + /** + * Iterator::rewind() + * + * Rewind always gets the new count from the storage. Thus if you use + * the interfaces and your scripts take long you should use reset() + * from time to time. + * + * @return void + */ + public function rewind() + { + $this->_iterationMax = $this->countMessages(); + $this->_iterationPos = 1; + } + + + /** + * Iterator::current() + * + * @return Zend_Mail_Message current message + */ + public function current() + { + return $this->getMessage($this->_iterationPos); + } + + + /** + * Iterator::key() + * + * @return int id of current position + */ + public function key() + { + return $this->_iterationPos; + } + + + /** + * Iterator::next() + * + * @return void + */ + public function next() + { + ++$this->_iterationPos; + } + + + /** + * Iterator::valid() + * + * @return boolean + */ + public function valid() + { + if ($this->_iterationMax === null) { + $this->_iterationMax = $this->countMessages(); + } + return $this->_iterationPos && $this->_iterationPos <= $this->_iterationMax; + } + + + /** + * SeekableIterator::seek() + * + * @param int $pos + * @return void + * @throws OutOfBoundsException + */ + public function seek($pos) + { + if ($this->_iterationMax === null) { + $this->_iterationMax = $this->countMessages(); + } + + if ($pos > $this->_iterationMax) { + throw new OutOfBoundsException('this position does not exist'); + } + $this->_iterationPos = $pos; + } + +} diff --git a/lib/Zend/Mail/Storage/Exception.php b/lib/Zend/Mail/Storage/Exception.php new file mode 100644 index 0000000..1aea4f9 --- /dev/null +++ b/lib/Zend/Mail/Storage/Exception.php @@ -0,0 +1,39 @@ + Zend_Mail_Storage_Folder folder) + * @var array + */ + protected $_folders; + + /** + * local name (name of folder in parent folder) + * @var string + */ + protected $_localName; + + /** + * global name (absolute name of folder) + * @var string + */ + protected $_globalName; + + /** + * folder is selectable if folder is able to hold messages, else it's just a parent folder + * @var bool + */ + protected $_selectable = true; + + /** + * create a new mail folder instance + * + * @param string $localName name of folder in current subdirectory + * @param string $globalName absolute name of folder + * @param bool $selectable if true folder holds messages, if false it's just a parent for subfolders + * @param array $folders init with given instances of Zend_Mail_Storage_Folder as subfolders + */ + public function __construct($localName, $globalName = '', $selectable = true, array $folders = array()) + { + $this->_localName = $localName; + $this->_globalName = $globalName ? $globalName : $localName; + $this->_selectable = $selectable; + $this->_folders = $folders; + } + + /** + * implements RecursiveIterator::hasChildren() + * + * @return bool current element has children + */ + public function hasChildren() + { + $current = $this->current(); + return $current && $current instanceof Zend_Mail_Storage_Folder && !$current->isLeaf(); + } + + /** + * implements RecursiveIterator::getChildren() + * + * @return Zend_Mail_Storage_Folder same as self::current() + */ + public function getChildren() + { + return $this->current(); + } + + /** + * implements Iterator::valid() + * + * @return bool check if there's a current element + */ + public function valid() + { + return key($this->_folders) !== null; + } + + /** + * implements Iterator::next() + * + * @return null + */ + public function next() + { + next($this->_folders); + } + + /** + * implements Iterator::key() + * + * @return string key/local name of current element + */ + public function key() + { + return key($this->_folders); + } + + /** + * implements Iterator::current() + * + * @return Zend_Mail_Storage_Folder current folder + */ + public function current() + { + return current($this->_folders); + } + + /** + * implements Iterator::rewind() + * + * @return null + */ + public function rewind() + { + reset($this->_folders); + } + + /** + * get subfolder named $name + * + * @param string $name wanted subfolder + * @return Zend_Mail_Storage_Folder folder named $folder + * @throws Zend_Mail_Storage_Exception + */ + public function __get($name) + { + if (!isset($this->_folders[$name])) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("no subfolder named $name"); + } + + return $this->_folders[$name]; + } + + /** + * add or replace subfolder named $name + * + * @param string $name local name of subfolder + * @param Zend_Mail_Storage_Folder $folder instance for new subfolder + * @return null + */ + public function __set($name, Zend_Mail_Storage_Folder $folder) + { + $this->_folders[$name] = $folder; + } + + /** + * remove subfolder named $name + * + * @param string $name local name of subfolder + * @return null + */ + public function __unset($name) + { + unset($this->_folders[$name]); + } + + /** + * magic method for easy output of global name + * + * @return string global name of folder + */ + public function __toString() + { + return (string)$this->getGlobalName(); + } + + /** + * get local name + * + * @return string local name + */ + public function getLocalName() + { + return $this->_localName; + } + + /** + * get global name + * + * @return string global name + */ + public function getGlobalName() + { + return $this->_globalName; + } + + /** + * is this folder selectable? + * + * @return bool selectable + */ + public function isSelectable() + { + return $this->_selectable; + } + + /** + * check if folder has no subfolder + * + * @return bool true if no subfolders + */ + public function isLeaf() + { + return empty($this->_folders); + } +} diff --git a/lib/Zend/Mail/Storage/Folder/Interface.php b/lib/Zend/Mail/Storage/Folder/Interface.php new file mode 100644 index 0000000..cb148b7 --- /dev/null +++ b/lib/Zend/Mail/Storage/Folder/Interface.php @@ -0,0 +1,60 @@ +dirname) || !is_dir($params->dirname)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('no valid dirname given in params'); + } + + $this->_rootdir = rtrim($params->dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + + $this->_delim = isset($params->delim) ? $params->delim : '.'; + + $this->_buildFolderTree(); + $this->selectFolder(!empty($params->folder) ? $params->folder : 'INBOX'); + $this->_has['top'] = true; + $this->_has['flags'] = true; + } + + /** + * find all subfolders and mbox files for folder structure + * + * Result is save in Zend_Mail_Storage_Folder instances with the root in $this->_rootFolder. + * $parentFolder and $parentGlobalName are only used internally for recursion. + * + * @return null + * @throws Zend_Mail_Storage_Exception + */ + protected function _buildFolderTree() + { + $this->_rootFolder = new Zend_Mail_Storage_Folder('/', '/', false); + $this->_rootFolder->INBOX = new Zend_Mail_Storage_Folder('INBOX', 'INBOX', true); + + $dh = @opendir($this->_rootdir); + if (!$dh) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("can't read folders in maildir"); + } + $dirs = array(); + while (($entry = readdir($dh)) !== false) { + // maildir++ defines folders must start with . + if ($entry[0] != '.' || $entry == '.' || $entry == '..') { + continue; + } + if ($this->_isMaildir($this->_rootdir . $entry)) { + $dirs[] = $entry; + } + } + closedir($dh); + + sort($dirs); + $stack = array(null); + $folderStack = array(null); + $parentFolder = $this->_rootFolder; + $parent = '.'; + + foreach ($dirs as $dir) { + do { + if (strpos($dir, $parent) === 0) { + $local = substr($dir, strlen($parent)); + if (strpos($local, $this->_delim) !== false) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('error while reading maildir'); + } + array_push($stack, $parent); + $parent = $dir . $this->_delim; + $folder = new Zend_Mail_Storage_Folder($local, substr($dir, 1), true); + $parentFolder->$local = $folder; + array_push($folderStack, $parentFolder); + $parentFolder = $folder; + break; + } else if ($stack) { + $parent = array_pop($stack); + $parentFolder = array_pop($folderStack); + } + } while ($stack); + if (!$stack) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('error while reading maildir'); + } + } + } + + /** + * get root folder or given folder + * + * @param string $rootFolder get folder structure for given folder, else root + * @return Zend_Mail_Storage_Folder root or wanted folder + * @throws Zend_Mail_Storage_Exception + */ + public function getFolders($rootFolder = null) + { + if (!$rootFolder || $rootFolder == 'INBOX') { + return $this->_rootFolder; + } + + // rootdir is same as INBOX in maildir + if (strpos($rootFolder, 'INBOX' . $this->_delim) === 0) { + $rootFolder = substr($rootFolder, 6); + } + $currentFolder = $this->_rootFolder; + $subname = trim($rootFolder, $this->_delim); + while ($currentFolder) { + @list($entry, $subname) = @explode($this->_delim, $subname, 2); + $currentFolder = $currentFolder->$entry; + if (!$subname) { + break; + } + } + + if ($currentFolder->getGlobalName() != rtrim($rootFolder, $this->_delim)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("folder $rootFolder not found"); + } + return $currentFolder; + } + + /** + * select given folder + * + * folder must be selectable! + * + * @param Zend_Mail_Storage_Folder|string $globalName global name of folder or instance for subfolder + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function selectFolder($globalName) + { + $this->_currentFolder = (string)$globalName; + + // getting folder from folder tree for validation + $folder = $this->getFolders($this->_currentFolder); + + try { + $this->_openMaildir($this->_rootdir . '.' . $folder->getGlobalName()); + } catch(Zend_Mail_Storage_Exception $e) { + // check what went wrong + if (!$folder->isSelectable()) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("{$this->_currentFolder} is not selectable"); + } + // seems like file has vanished; rebuilding folder tree - but it's still an exception + $this->_buildFolderTree($this->_rootdir); + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('seems like the maildir has vanished, I\'ve rebuild the ' . + 'folder tree, search for an other folder and try again'); + } + } + + /** + * get Zend_Mail_Storage_Folder instance for current folder + * + * @return Zend_Mail_Storage_Folder instance of current folder + * @throws Zend_Mail_Storage_Exception + */ + public function getCurrentFolder() + { + return $this->_currentFolder; + } +} diff --git a/lib/Zend/Mail/Storage/Folder/Mbox.php b/lib/Zend/Mail/Storage/Folder/Mbox.php new file mode 100644 index 0000000..797148d --- /dev/null +++ b/lib/Zend/Mail/Storage/Folder/Mbox.php @@ -0,0 +1,264 @@ +filename)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('use Zend_Mail_Storage_Mbox for a single file'); + } + + if (!isset($params->dirname) || !is_dir($params->dirname)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('no valid dirname given in params'); + } + + $this->_rootdir = rtrim($params->dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + + $this->_buildFolderTree($this->_rootdir); + $this->selectFolder(!empty($params->folder) ? $params->folder : 'INBOX'); + $this->_has['top'] = true; + $this->_has['uniqueid'] = false; + } + + /** + * find all subfolders and mbox files for folder structure + * + * Result is save in Zend_Mail_Storage_Folder instances with the root in $this->_rootFolder. + * $parentFolder and $parentGlobalName are only used internally for recursion. + * + * @param string $currentDir call with root dir, also used for recursion. + * @param Zend_Mail_Storage_Folder|null $parentFolder used for recursion + * @param string $parentGlobalName used for rescursion + * @return null + * @throws Zend_Mail_Storage_Exception + */ + protected function _buildFolderTree($currentDir, $parentFolder = null, $parentGlobalName = '') + { + if (!$parentFolder) { + $this->_rootFolder = new Zend_Mail_Storage_Folder('/', '/', false); + $parentFolder = $this->_rootFolder; + } + + $dh = @opendir($currentDir); + if (!$dh) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("can't read dir $currentDir"); + } + while (($entry = readdir($dh)) !== false) { + // ignore hidden files for mbox + if ($entry[0] == '.') { + continue; + } + $absoluteEntry = $currentDir . $entry; + $globalName = $parentGlobalName . DIRECTORY_SEPARATOR . $entry; + if (is_file($absoluteEntry) && $this->_isMboxFile($absoluteEntry)) { + $parentFolder->$entry = new Zend_Mail_Storage_Folder($entry, $globalName); + continue; + } + if (!is_dir($absoluteEntry) /* || $entry == '.' || $entry == '..' */) { + continue; + } + $folder = new Zend_Mail_Storage_Folder($entry, $globalName, false); + $parentFolder->$entry = $folder; + $this->_buildFolderTree($absoluteEntry . DIRECTORY_SEPARATOR, $folder, $globalName); + } + + closedir($dh); + } + + /** + * get root folder or given folder + * + * @param string $rootFolder get folder structure for given folder, else root + * @return Zend_Mail_Storage_Folder root or wanted folder + * @throws Zend_Mail_Storage_Exception + */ + public function getFolders($rootFolder = null) + { + if (!$rootFolder) { + return $this->_rootFolder; + } + + $currentFolder = $this->_rootFolder; + $subname = trim($rootFolder, DIRECTORY_SEPARATOR); + while ($currentFolder) { + @list($entry, $subname) = @explode(DIRECTORY_SEPARATOR, $subname, 2); + $currentFolder = $currentFolder->$entry; + if (!$subname) { + break; + } + } + + if ($currentFolder->getGlobalName() != DIRECTORY_SEPARATOR . trim($rootFolder, DIRECTORY_SEPARATOR)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("folder $rootFolder not found"); + } + return $currentFolder; + } + + /** + * select given folder + * + * folder must be selectable! + * + * @param Zend_Mail_Storage_Folder|string $globalName global name of folder or instance for subfolder + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function selectFolder($globalName) + { + $this->_currentFolder = (string)$globalName; + + // getting folder from folder tree for validation + $folder = $this->getFolders($this->_currentFolder); + + try { + $this->_openMboxFile($this->_rootdir . $folder->getGlobalName()); + } catch(Zend_Mail_Storage_Exception $e) { + // check what went wrong + if (!$folder->isSelectable()) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("{$this->_currentFolder} is not selectable"); + } + // seems like file has vanished; rebuilding folder tree - but it's still an exception + $this->_buildFolderTree($this->_rootdir); + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('seems like the mbox file has vanished, I\'ve rebuild the ' . + 'folder tree, search for an other folder and try again'); + } + } + + /** + * get Zend_Mail_Storage_Folder instance for current folder + * + * @return Zend_Mail_Storage_Folder instance of current folder + * @throws Zend_Mail_Storage_Exception + */ + public function getCurrentFolder() + { + return $this->_currentFolder; + } + + /** + * magic method for serialize() + * + * with this method you can cache the mbox class + * + * @return array name of variables + */ + public function __sleep() + { + return array_merge(parent::__sleep(), array('_currentFolder', '_rootFolder', '_rootdir')); + } + + /** + * magic method for unserialize() + * + * with this method you can cache the mbox class + * + * @return null + */ + public function __wakeup() + { + // if cache is stall selectFolder() rebuilds the tree on error + parent::__wakeup(); + } +} diff --git a/lib/Zend/Mail/Storage/Imap.php b/lib/Zend/Mail/Storage/Imap.php new file mode 100644 index 0000000..f4051f9 --- /dev/null +++ b/lib/Zend/Mail/Storage/Imap.php @@ -0,0 +1,644 @@ + Zend_Mail_Storage::FLAG_PASSED, + '\Answered' => Zend_Mail_Storage::FLAG_ANSWERED, + '\Seen' => Zend_Mail_Storage::FLAG_SEEN, + '\Deleted' => Zend_Mail_Storage::FLAG_DELETED, + '\Draft' => Zend_Mail_Storage::FLAG_DRAFT, + '\Flagged' => Zend_Mail_Storage::FLAG_FLAGGED); + + /** + * map flags to search criterias + * @var array + */ + protected static $_searchFlags = array('\Recent' => 'RECENT', + '\Answered' => 'ANSWERED', + '\Seen' => 'SEEN', + '\Deleted' => 'DELETED', + '\Draft' => 'DRAFT', + '\Flagged' => 'FLAGGED'); + + /** + * Count messages all messages in current box + * + * @return int number of messages + * @throws Zend_Mail_Storage_Exception + * @throws Zend_Mail_Protocol_Exception + */ + public function countMessages($flags = null) + { + if (!$this->_currentFolder) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('No selected folder to count'); + } + + if ($flags === null) { + return count($this->_protocol->search(array('ALL'))); + } + + $params = array(); + foreach ((array)$flags as $flag) { + if (isset(self::$_searchFlags[$flag])) { + $params[] = self::$_searchFlags[$flag]; + } else { + $params[] = 'KEYWORD'; + $params[] = $this->_protocol->escapeString($flag); + } + } + return count($this->_protocol->search($params)); + } + + /** + * get a list of messages with number and size + * + * @param int $id number of message + * @return int|array size of given message of list with all messages as array(num => size) + * @throws Zend_Mail_Protocol_Exception + */ + public function getSize($id = 0) + { + if ($id) { + return $this->_protocol->fetch('RFC822.SIZE', $id); + } + return $this->_protocol->fetch('RFC822.SIZE', 1, INF); + } + + /** + * Fetch a message + * + * @param int $id number of message + * @return Zend_Mail_Message + * @throws Zend_Mail_Protocol_Exception + */ + public function getMessage($id) + { + $data = $this->_protocol->fetch(array('FLAGS', 'RFC822.HEADER'), $id); + $header = $data['RFC822.HEADER']; + + $flags = array(); + foreach ($data['FLAGS'] as $flag) { + $flags[] = isset(self::$_knownFlags[$flag]) ? self::$_knownFlags[$flag] : $flag; + } + + return new $this->_messageClass(array('handler' => $this, 'id' => $id, 'headers' => $header, 'flags' => $flags)); + } + + /* + * Get raw header of message or part + * + * @param int $id number of message + * @param null|array|string $part path to part or null for messsage header + * @param int $topLines include this many lines with header (after an empty line) + * @param int $topLines include this many lines with header (after an empty line) + * @return string raw header + * @throws Zend_Mail_Protocol_Exception + * @throws Zend_Mail_Storage_Exception + */ + public function getRawHeader($id, $part = null, $topLines = 0) + { + if ($part !== null) { + // TODO: implement + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('not implemented'); + } + + // TODO: toplines + return $this->_protocol->fetch('RFC822.HEADER', $id); + } + + /* + * Get raw content of message or part + * + * @param int $id number of message + * @param null|array|string $part path to part or null for messsage content + * @return string raw content + * @throws Zend_Mail_Protocol_Exception + * @throws Zend_Mail_Storage_Exception + */ + public function getRawContent($id, $part = null) + { + if ($part !== null) { + // TODO: implement + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('not implemented'); + } + + return $this->_protocol->fetch('RFC822.TEXT', $id); + } + + /** + * create instance with parameters + * Supported paramters are + * - user username + * - host hostname or ip address of IMAP server [optional, default = 'localhost'] + * - password password for user 'username' [optional, default = ''] + * - port port for IMAP server [optional, default = 110] + * - ssl 'SSL' or 'TLS' for secure sockets + * - folder select this folder [optional, default = 'INBOX'] + * + * @param array $params mail reader specific parameters + * @throws Zend_Mail_Storage_Exception + * @throws Zend_Mail_Protocol_Exception + */ + public function __construct($params) + { + if (is_array($params)) { + $params = (object)$params; + } + + $this->_has['flags'] = true; + + if ($params instanceof Zend_Mail_Protocol_Imap) { + $this->_protocol = $params; + try { + $this->selectFolder('INBOX'); + } catch(Zend_Mail_Storage_Exception $e) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot select INBOX, is this a valid transport?'); + } + return; + } + + if (!isset($params->user)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('need at least user in params'); + } + + $host = isset($params->host) ? $params->host : 'localhost'; + $password = isset($params->password) ? $params->password : ''; + $port = isset($params->port) ? $params->port : null; + $ssl = isset($params->ssl) ? $params->ssl : false; + + $this->_protocol = new Zend_Mail_Protocol_Imap(); + $this->_protocol->connect($host, $port, $ssl); + if (!$this->_protocol->login($params->user, $password)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot login, user or password wrong'); + } + $this->selectFolder(isset($params->folder) ? $params->folder : 'INBOX'); + } + + /** + * Close resource for mail lib. If you need to control, when the resource + * is closed. Otherwise the destructor would call this. + * + * @return null + */ + public function close() + { + $this->_currentFolder = ''; + $this->_protocol->logout(); + } + + /** + * Keep the server busy. + * + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function noop() + { + if (!$this->_protocol->noop()) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('could not do nothing'); + } + } + + /** + * Remove a message from server. If you're doing that from a web enviroment + * you should be careful and use a uniqueid as parameter if possible to + * identify the message. + * + * @param int $id number of message + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function removeMessage($id) + { + if (!$this->_protocol->store(array(Zend_Mail_Storage::FLAG_DELETED), $id, null, '+')) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot set deleted flag'); + } + // TODO: expunge here or at close? we can handle an error here better and are more fail safe + if (!$this->_protocol->expunge()) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('message marked as deleted, but could not expunge'); + } + } + + /** + * get unique id for one or all messages + * + * if storage does not support unique ids it's the same as the message number + * + * @param int|null $id message number + * @return array|string message number for given message or all messages as array + * @throws Zend_Mail_Storage_Exception + */ + public function getUniqueId($id = null) + { + if ($id) { + return $this->_protocol->fetch('UID', $id); + } + + return $this->_protocol->fetch('UID', 1, INF); + } + + /** + * get a message number from a unique id + * + * I.e. if you have a webmailer that supports deleting messages you should use unique ids + * as parameter and use this method to translate it to message number right before calling removeMessage() + * + * @param string $id unique id + * @return int message number + * @throws Zend_Mail_Storage_Exception + */ + public function getNumberByUniqueId($id) + { + // TODO: use search to find number directly + $ids = $this->getUniqueId(); + foreach ($ids as $k => $v) { + if ($v == $id) { + return $k; + } + } + + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('unique id not found'); + } + + + /** + * get root folder or given folder + * + * @param string $rootFolder get folder structure for given folder, else root + * @return Zend_Mail_Storage_Folder root or wanted folder + * @throws Zend_Mail_Storage_Exception + * @throws Zend_Mail_Protocol_Exception + */ + public function getFolders($rootFolder = null) + { + $folders = $this->_protocol->listMailbox((string)$rootFolder); + if (!$folders) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('folder not found'); + } + + ksort($folders, SORT_STRING); + $root = new Zend_Mail_Storage_Folder('/', '/', false); + $stack = array(null); + $folderStack = array(null); + $parentFolder = $root; + $parent = ''; + + foreach ($folders as $globalName => $data) { + do { + if (!$parent || strpos($globalName, $parent) === 0) { + $pos = strrpos($globalName, $data['delim']); + if ($pos === false) { + $localName = $globalName; + } else { + $localName = substr($globalName, $pos + 1); + } + $selectable = !$data['flags'] || !in_array('\\Noselect', $data['flags']); + + array_push($stack, $parent); + $parent = $globalName . $data['delim']; + $folder = new Zend_Mail_Storage_Folder($localName, $globalName, $selectable); + $parentFolder->$localName = $folder; + array_push($folderStack, $parentFolder); + $parentFolder = $folder; + break; + } else if ($stack) { + $parent = array_pop($stack); + $parentFolder = array_pop($folderStack); + } + } while ($stack); + if (!$stack) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('error while constructing folder tree'); + } + } + + return $root; + } + + /** + * select given folder + * + * folder must be selectable! + * + * @param Zend_Mail_Storage_Folder|string $globalName global name of folder or instance for subfolder + * @return null + * @throws Zend_Mail_Storage_Exception + * @throws Zend_Mail_Protocol_Exception + */ + public function selectFolder($globalName) + { + $this->_currentFolder = $globalName; + if (!$this->_protocol->select($this->_currentFolder)) { + $this->_currentFolder = ''; + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot change folder, maybe it does not exist'); + } + } + + + /** + * get Zend_Mail_Storage_Folder instance for current folder + * + * @return Zend_Mail_Storage_Folder instance of current folder + * @throws Zend_Mail_Storage_Exception + */ + public function getCurrentFolder() + { + return $this->_currentFolder; + } + + /** + * create a new folder + * + * This method also creates parent folders if necessary. Some mail storages may restrict, which folder + * may be used as parent or which chars may be used in the folder name + * + * @param string $name global name of folder, local name if $parentFolder is set + * @param string|Zend_Mail_Storage_Folder $parentFolder parent folder for new folder, else root folder is parent + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function createFolder($name, $parentFolder = null) + { + // TODO: we assume / as the hierarchy delim - need to get that from the folder class! + if ($parentFolder instanceof Zend_Mail_Storage_Folder) { + $folder = $parentFolder->getGlobalName() . '/' . $name; + } else if ($parentFolder != null) { + $folder = $parentFolder . '/' . $name; + } else { + $folder = $name; + } + + if (!$this->_protocol->create($folder)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot create folder'); + } + } + + /** + * remove a folder + * + * @param string|Zend_Mail_Storage_Folder $name name or instance of folder + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function removeFolder($name) + { + if ($name instanceof Zend_Mail_Storage_Folder) { + $name = $name->getGlobalName(); + } + + if (!$this->_protocol->delete($name)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot delete folder'); + } + } + + /** + * rename and/or move folder + * + * The new name has the same restrictions as in createFolder() + * + * @param string|Zend_Mail_Storage_Folder $oldName name or instance of folder + * @param string $newName new global name of folder + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function renameFolder($oldName, $newName) + { + if ($oldName instanceof Zend_Mail_Storage_Folder) { + $oldName = $oldName->getGlobalName(); + } + + if (!$this->_protocol->rename($oldName, $newName)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot rename folder'); + } + } + + /** + * append a new message to mail storage + * + * @param string $message message as string or instance of message class + * @param null|string|Zend_Mail_Storage_Folder $folder folder for new message, else current folder is taken + * @param null|array $flags set flags for new message, else a default set is used + * @throws Zend_Mail_Storage_Exception + */ + // not yet * @param string|Zend_Mail_Message|Zend_Mime_Message $message message as string or instance of message class + public function appendMessage($message, $folder = null, $flags = null) + { + if ($folder === null) { + $folder = $this->_currentFolder; + } + + if ($flags === null) { + $flags = array(Zend_Mail_Storage::FLAG_SEEN); + } + + // TODO: handle class instances for $message + if (!$this->_protocol->append($folder, $message, $flags)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot create message, please check if the folder exists and your flags'); + } + } + + /** + * copy an existing message + * + * @param int $id number of message + * @param string|Zend_Mail_Storage_Folder $folder name or instance of targer folder + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function copyMessage($id, $folder) + { + if (!$this->_protocol->copy($folder, $id)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot copy message, does the folder exist?'); + } + } + + /** + * move an existing message + * + * NOTE: imap has no native move command, thus it's emulated with copy and delete + * + * @param int $id number of message + * @param string|Zend_Mail_Storage_Folder $folder name or instance of targer folder + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function moveMessage($id, $folder) { + $this->copyMessage($id, $folder); + $this->removeMessage($id); + } + + /** + * set flags for message + * + * NOTE: this method can't set the recent flag. + * + * @param int $id number of message + * @param array $flags new flags for message + * @throws Zend_Mail_Storage_Exception + */ + public function setFlags($id, $flags) + { + if (!$this->_protocol->store($flags, $id)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot set flags, have you tried to set the recent flag or special chars?'); + } + } +} + diff --git a/lib/Zend/Mail/Storage/Maildir.php b/lib/Zend/Mail/Storage/Maildir.php new file mode 100644 index 0000000..429636a --- /dev/null +++ b/lib/Zend/Mail/Storage/Maildir.php @@ -0,0 +1,475 @@ + Zend_Mail_Storage::FLAG_DRAFT, + 'F' => Zend_Mail_Storage::FLAG_FLAGGED, + 'P' => Zend_Mail_Storage::FLAG_PASSED, + 'R' => Zend_Mail_Storage::FLAG_ANSWERED, + 'S' => Zend_Mail_Storage::FLAG_SEEN, + 'T' => Zend_Mail_Storage::FLAG_DELETED); + + // TODO: getFlags($id) for fast access if headers are not needed (i.e. just setting flags)? + + /** + * Count messages all messages in current box + * + * @return int number of messages + * @throws Zend_Mail_Storage_Exception + */ + public function countMessages($flags = null) + { + if ($flags === null) { + return count($this->_files); + } + + $count = 0; + if (!is_array($flags)) { + foreach ($this->_files as $file) { + if (isset($file['flaglookup'][$flags])) { + ++$count; + } + } + return $count; + } + + $flags = array_flip($flags); + foreach ($this->_files as $file) { + foreach ($flags as $flag => $v) { + if (!isset($file['flaglookup'][$flag])) { + continue 2; + } + } + ++$count; + } + return $count; + } + + /** + * Get one or all fields from file structure. Also checks if message is valid + * + * @param int $id message number + * @param string|null $field wanted field + * @return string|array wanted field or all fields as array + * @throws Zend_Mail_Storage_Exception + */ + protected function _getFileData($id, $field = null) + { + if (!isset($this->_files[$id - 1])) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('id does not exist'); + } + + if (!$field) { + return $this->_files[$id - 1]; + } + + if (!isset($this->_files[$id - 1][$field])) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('field does not exist'); + } + + return $this->_files[$id - 1][$field]; + } + + /** + * Get a list of messages with number and size + * + * @param int|null $id number of message or null for all messages + * @return int|array size of given message of list with all messages as array(num => size) + * @throws Zend_Mail_Storage_Exception + */ + public function getSize($id = null) + { + if ($id !== null) { + $filedata = $this->_getFileData($id); + return isset($filedata['size']) ? $filedata['size'] : filesize($filedata['filename']); + } + + $result = array(); + foreach ($this->_files as $num => $data) { + $result[$num + 1] = isset($data['size']) ? $data['size'] : filesize($data['filename']); + } + + return $result; + } + + + + /** + * Fetch a message + * + * @param int $id number of message + * @return Zend_Mail_Message_File + * @throws Zend_Mail_Storage_Exception + */ + public function getMessage($id) + { + // TODO that's ugly, would be better to let the message class decide + if (strtolower($this->_messageClass) == 'zend_mail_message_file' || is_subclass_of($this->_messageClass, 'zend_mail_message_file')) { + return new $this->_messageClass(array('file' => $this->_getFileData($id, 'filename'), + 'flags' => $this->_getFileData($id, 'flags'))); + } + + return new $this->_messageClass(array('handler' => $this, 'id' => $id, 'headers' => $this->getRawHeader($id), + 'flags' => $this->_getFileData($id, 'flags'))); + } + + /* + * Get raw header of message or part + * + * @param int $id number of message + * @param null|array|string $part path to part or null for messsage header + * @param int $topLines include this many lines with header (after an empty line) + * @return string raw header + * @throws Zend_Mail_Storage_Exception + */ + public function getRawHeader($id, $part = null, $topLines = 0) + { + if ($part !== null) { + // TODO: implement + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('not implemented'); + } + + $fh = fopen($this->_getFileData($id, 'filename'), 'r'); + + $content = ''; + while (!feof($fh)) { + $line = fgets($fh); + if (!trim($line)) { + break; + } + $content .= $line; + } + + fclose($fh); + return $content; + } + + /* + * Get raw content of message or part + * + * @param int $id number of message + * @param null|array|string $part path to part or null for messsage content + * @return string raw content + * @throws Zend_Mail_Storage_Exception + */ + public function getRawContent($id, $part = null) + { + if ($part !== null) { + // TODO: implement + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('not implemented'); + } + + $fh = fopen($this->_getFileData($id, 'filename'), 'r'); + + while (!feof($fh)) { + $line = fgets($fh); + if (!trim($line)) { + break; + } + } + + $content = stream_get_contents($fh); + fclose($fh); + return $content; + } + + /** + * Create instance with parameters + * Supported parameters are: + * - dirname dirname of mbox file + * + * @param $params array mail reader specific parameters + * @throws Zend_Mail_Storage_Exception + */ + public function __construct($params) + { + if (is_array($params)) { + $params = (object)$params; + } + + if (!isset($params->dirname) || !is_dir($params->dirname)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('no valid dirname given in params'); + } + + if (!$this->_isMaildir($params->dirname)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('invalid maildir given'); + } + + $this->_has['top'] = true; + $this->_has['flags'] = true; + $this->_openMaildir($params->dirname); + } + + /** + * check if a given dir is a valid maildir + * + * @param string $dirname name of dir + * @return bool dir is valid maildir + */ + protected function _isMaildir($dirname) + { + if (file_exists($dirname . '/new') && !is_dir($dirname . '/new')) { + return false; + } + if (file_exists($dirname . '/tmp') && !is_dir($dirname . '/tmp')) { + return false; + } + return is_dir($dirname . '/cur'); + } + + /** + * open given dir as current maildir + * + * @param string $dirname name of maildir + * @return null + * @throws Zend_Mail_Storage_Exception + */ + protected function _openMaildir($dirname) + { + if ($this->_files) { + $this->close(); + } + + $dh = @opendir($dirname . '/cur/'); + if (!$dh) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot open maildir'); + } + $this->_getMaildirFiles($dh, $dirname . '/cur/'); + closedir($dh); + + $dh = @opendir($dirname . '/new/'); + if ($dh) { + $this->_getMaildirFiles($dh, $dirname . '/new/', array(Zend_Mail_Storage::FLAG_RECENT)); + closedir($dh); + } else if (file_exists($dirname . '/new/')) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot read recent mails in maildir'); + } + } + + /** + * find all files in opened dir handle and add to maildir files + * + * @param resource $dh dir handle used for search + * @param string $dirname dirname of dir in $dh + * @param array $default_flags default flags for given dir + * @return null + */ + protected function _getMaildirFiles($dh, $dirname, $default_flags = array()) + { + while (($entry = readdir($dh)) !== false) { + if ($entry[0] == '.' || !is_file($dirname . $entry)) { + continue; + } + + @list($uniq, $info) = explode(':', $entry, 2); + @list(,$size) = explode(',', $uniq, 2); + if ($size && $size[0] == 'S' && $size[1] == '=') { + $size = substr($size, 2); + } + if (!ctype_digit($size)) { + $size = null; + } + @list($version, $flags) = explode(',', $info, 2); + if ($version != 2) { + $flags = ''; + } + + $named_flags = $default_flags; + $length = strlen($flags); + for ($i = 0; $i < $length; ++$i) { + $flag = $flags[$i]; + $named_flags[$flag] = isset(self::$_knownFlags[$flag]) ? self::$_knownFlags[$flag] : $flag; + } + + $data = array('uniq' => $uniq, + 'flags' => $named_flags, + 'flaglookup' => array_flip($named_flags), + 'filename' => $dirname . $entry); + if ($size !== null) { + $data['size'] = (int)$size; + } + $this->_files[] = $data; + } + } + + + /** + * Close resource for mail lib. If you need to control, when the resource + * is closed. Otherwise the destructor would call this. + * + * @return void + */ + public function close() + { + $this->_files = array(); + } + + + /** + * Waste some CPU cycles doing nothing. + * + * @return void + */ + public function noop() + { + return true; + } + + + /** + * stub for not supported message deletion + * + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function removeMessage($id) + { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('maildir is (currently) read-only'); + } + + /** + * get unique id for one or all messages + * + * if storage does not support unique ids it's the same as the message number + * + * @param int|null $id message number + * @return array|string message number for given message or all messages as array + * @throws Zend_Mail_Storage_Exception + */ + public function getUniqueId($id = null) + { + if ($id) { + return $this->_getFileData($id, 'uniq'); + } + + $ids = array(); + foreach ($this->_files as $num => $file) { + $ids[$num + 1] = $file['uniq']; + } + return $ids; + } + + /** + * get a message number from a unique id + * + * I.e. if you have a webmailer that supports deleting messages you should use unique ids + * as parameter and use this method to translate it to message number right before calling removeMessage() + * + * @param string $id unique id + * @return int message number + * @throws Zend_Mail_Storage_Exception + */ + public function getNumberByUniqueId($id) + { + foreach ($this->_files as $num => $file) { + if ($file['uniq'] == $id) { + return $num + 1; + } + } + + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('unique id not found'); + } +} diff --git a/lib/Zend/Mail/Storage/Mbox.php b/lib/Zend/Mail/Storage/Mbox.php new file mode 100644 index 0000000..8be42d8 --- /dev/null +++ b/lib/Zend/Mail/Storage/Mbox.php @@ -0,0 +1,447 @@ + start, 'seperator' => headersep, 'end' => end) + * @var array + */ + protected $_positions; + + /** + * used message class, change it in an extened class to extend the returned message class + * @var string + */ + protected $_messageClass = 'Zend_Mail_Message_File'; + + /** + * Count messages all messages in current box + * + * @return int number of messages + * @throws Zend_Mail_Storage_Exception + */ + public function countMessages() + { + return count($this->_positions); + } + + + /** + * Get a list of messages with number and size + * + * @param int|null $id number of message or null for all messages + * @return int|array size of given message of list with all messages as array(num => size) + */ + public function getSize($id = 0) + { + if ($id) { + $pos = $this->_positions[$id - 1]; + return $pos['end'] - $pos['start']; + } + + $result = array(); + foreach ($this->_positions as $num => $pos) { + $result[$num + 1] = $pos['end'] - $pos['start']; + } + + return $result; + } + + + /** + * Get positions for mail message or throw exeption if id is invalid + * + * @param int $id number of message + * @return array positions as in _positions + * @throws Zend_Mail_Storage_Exception + */ + protected function _getPos($id) + { + if (!isset($this->_positions[$id - 1])) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('id does not exist'); + } + + return $this->_positions[$id - 1]; + } + + + /** + * Fetch a message + * + * @param int $id number of message + * @return Zend_Mail_Message_File + * @throws Zend_Mail_Storage_Exception + */ + public function getMessage($id) + { + // TODO that's ugly, would be better to let the message class decide + if (strtolower($this->_messageClass) == 'zend_mail_message_file' || is_subclass_of($this->_messageClass, 'zend_mail_message_file')) { + // TODO top/body lines + $messagePos = $this->_getPos($id); + return new $this->_messageClass(array('file' => $this->_fh, 'startPos' => $messagePos['start'], + 'endPos' => $messagePos['end'])); + } + + $bodyLines = 0; // TODO: need a way to change that + + $message = $this->getRawHeader($id); + // file pointer is after headers now + if ($bodyLines) { + $message .= "\n"; + while ($bodyLines-- && ftell($this->_fh) < $this->_positions[$id - 1]['end']) { + $message .= fgets($this->_fh); + } + } + + return new $this->_messageClass(array('handler' => $this, 'id' => $id, 'headers' => $message)); + } + + /* + * Get raw header of message or part + * + * @param int $id number of message + * @param null|array|string $part path to part or null for messsage header + * @param int $topLines include this many lines with header (after an empty line) + * @return string raw header + * @throws Zend_Mail_Protocol_Exception + * @throws Zend_Mail_Storage_Exception + */ + public function getRawHeader($id, $part = null, $topLines = 0) + { + if ($part !== null) { + // TODO: implement + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('not implemented'); + } + $messagePos = $this->_getPos($id); + // TODO: toplines + return stream_get_contents($this->_fh, $messagePos['separator'] - $messagePos['start'], $messagePos['start']); + } + + /* + * Get raw content of message or part + * + * @param int $id number of message + * @param null|array|string $part path to part or null for messsage content + * @return string raw content + * @throws Zend_Mail_Protocol_Exception + * @throws Zend_Mail_Storage_Exception + */ + public function getRawContent($id, $part = null) + { + if ($part !== null) { + // TODO: implement + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('not implemented'); + } + $messagePos = $this->_getPos($id); + return stream_get_contents($this->_fh, $messagePos['end'] - $messagePos['separator'], $messagePos['separator']); + } + + /** + * Create instance with parameters + * Supported parameters are: + * - filename filename of mbox file + * + * @param $params array mail reader specific parameters + * @throws Zend_Mail_Storage_Exception + */ + public function __construct($params) + { + if (is_array($params)) { + $params = (object)$params; + } + + if (!isset($params->filename) /* || Zend_Loader::isReadable($params['filename']) */) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('no valid filename given in params'); + } + + $this->_openMboxFile($params->filename); + $this->_has['top'] = true; + $this->_has['uniqueid'] = false; + } + + /** + * check if given file is a mbox file + * + * if $file is a resource its file pointer is moved after the first line + * + * @param resource|string $file stream resource of name of file + * @param bool $fileIsString file is string or resource + * @return bool file is mbox file + */ + protected function _isMboxFile($file, $fileIsString = true) + { + if ($fileIsString) { + $file = @fopen($file, 'r'); + if (!$file) { + return false; + } + } else { + fseek($file, 0); + } + + $result = false; + + $line = fgets($file); + if (strpos($line, 'From ') === 0) { + $result = true; + } + + if ($fileIsString) { + @fclose($file); + } + + return $result; + } + + /** + * open given file as current mbox file + * + * @param string $filename filename of mbox file + * @return null + * @throws Zend_Mail_Storage_Exception + */ + protected function _openMboxFile($filename) + { + if ($this->_fh) { + $this->close(); + } + + $this->_fh = @fopen($filename, 'r'); + if (!$this->_fh) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot open mbox file'); + } + $this->_filename = $filename; + $this->_filemtime = filemtime($this->_filename); + + if (!$this->_isMboxFile($this->_fh, false)) { + @fclose($this->_fh); + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('file is not a valid mbox format'); + } + + $messagePos = array('start' => ftell($this->_fh), 'separator' => 0, 'end' => 0); + while (($line = fgets($this->_fh)) !== false) { + if (strpos($line, 'From ') === 0) { + $messagePos['end'] = ftell($this->_fh) - strlen($line) - 2; // + newline + if (!$messagePos['separator']) { + $messagePos['separator'] = $messagePos['end']; + } + $this->_positions[] = $messagePos; + $messagePos = array('start' => ftell($this->_fh), 'separator' => 0, 'end' => 0); + } + if (!$messagePos['separator'] && !trim($line)) { + $messagePos['separator'] = ftell($this->_fh); + } + } + + $messagePos['end'] = ftell($this->_fh); + if (!$messagePos['separator']) { + $messagePos['separator'] = $messagePos['end']; + } + $this->_positions[] = $messagePos; + } + + /** + * Close resource for mail lib. If you need to control, when the resource + * is closed. Otherwise the destructor would call this. + * + * @return void + */ + public function close() + { + @fclose($this->_fh); + $this->_positions = array(); + } + + + /** + * Waste some CPU cycles doing nothing. + * + * @return void + */ + public function noop() + { + return true; + } + + + /** + * stub for not supported message deletion + * + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function removeMessage($id) + { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('mbox is read-only'); + } + + /** + * get unique id for one or all messages + * + * Mbox does not support unique ids (yet) - it's always the same as the message number. + * That shouldn't be a problem, because we can't change mbox files. Therefor the message + * number is save enough. + * + * @param int|null $id message number + * @return array|string message number for given message or all messages as array + * @throws Zend_Mail_Storage_Exception + */ + public function getUniqueId($id = null) + { + if ($id) { + // check if id exists + $this->_getPos($id); + return $id; + } + + $range = range(1, $this->countMessages()); + return array_combine($range, $range); + } + + /** + * get a message number from a unique id + * + * I.e. if you have a webmailer that supports deleting messages you should use unique ids + * as parameter and use this method to translate it to message number right before calling removeMessage() + * + * @param string $id unique id + * @return int message number + * @throws Zend_Mail_Storage_Exception + */ + public function getNumberByUniqueId($id) + { + // check if id exists + $this->_getPos($id); + return $id; + } + + /** + * magic method for serialize() + * + * with this method you can cache the mbox class + * + * @return array name of variables + */ + public function __sleep() + { + return array('_filename', '_positions', '_filemtime'); + } + + /** + * magic method for unserialize() + * + * with this method you can cache the mbox class + * for cache validation the mtime of the mbox file is used + * + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function __wakeup() + { + if ($this->_filemtime != @filemtime($this->_filename)) { + $this->close(); + $this->_openMboxFile($this->_filename); + } else { + $this->_fh = @fopen($this->_filename, 'r'); + if (!$this->_fh) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot open mbox file'); + } + } + } + +} diff --git a/lib/Zend/Mail/Storage/Pop3.php b/lib/Zend/Mail/Storage/Pop3.php new file mode 100644 index 0000000..693384d --- /dev/null +++ b/lib/Zend/Mail/Storage/Pop3.php @@ -0,0 +1,328 @@ +_protocol->status($count, $null); + return (int)$count; + } + + /** + * get a list of messages with number and size + * + * @param int $id number of message + * @return int|array size of given message of list with all messages as array(num => size) + * @throws Zend_Mail_Protocol_Exception + */ + public function getSize($id = 0) + { + $id = $id ? $id : null; + return $this->_protocol->getList($id); + } + + /** + * Fetch a message + * + * @param int $id number of message + * @return Zend_Mail_Message + * @throws Zend_Mail_Protocol_Exception + */ + public function getMessage($id) + { + $bodyLines = 0; + $message = $this->_protocol->top($id, $bodyLines, true); + + return new $this->_messageClass(array('handler' => $this, 'id' => $id, 'headers' => $message, + 'noToplines' => $bodyLines < 1)); + } + + /* + * Get raw header of message or part + * + * @param int $id number of message + * @param null|array|string $part path to part or null for messsage header + * @param int $topLines include this many lines with header (after an empty line) + * @return string raw header + * @throws Zend_Mail_Protocol_Exception + * @throws Zend_Mail_Storage_Exception + */ + public function getRawHeader($id, $part = null, $topLines = 0) + { + if ($part !== null) { + // TODO: implement + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('not implemented'); + } + + return $this->_protocol->top($id, 0, true); + } + + /* + * Get raw content of message or part + * + * @param int $id number of message + * @param null|array|string $part path to part or null for messsage content + * @return string raw content + * @throws Zend_Mail_Protocol_Exception + * @throws Zend_Mail_Storage_Exception + */ + public function getRawContent($id, $part = null) + { + if ($part !== null) { + // TODO: implement + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('not implemented'); + } + + $content = $this->_protocol->retrieve($id); + // TODO: find a way to avoid decoding the headers + Zend_Mime_Decode::splitMessage($content, $null, $body); + return $body; + } + + /** + * create instance with parameters + * Supported paramters are + * - host hostname or ip address of POP3 server + * - user username + * - password password for user 'username' [optional, default = ''] + * - port port for POP3 server [optional, default = 110] + * - ssl 'SSL' or 'TLS' for secure sockets + * + * @param $params array mail reader specific parameters + * @throws Zend_Mail_Storage_Exception + * @throws Zend_Mail_Protocol_Exception + */ + public function __construct($params) + { + if (is_array($params)) { + $params = (object)$params; + } + + $this->_has['fetchPart'] = false; + $this->_has['top'] = null; + $this->_has['uniqueid'] = null; + + if ($params instanceof Zend_Mail_Protocol_Pop3) { + $this->_protocol = $params; + return; + } + + if (!isset($params->user)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('need at least user in params'); + } + + $host = isset($params->host) ? $params->host : 'localhost'; + $password = isset($params->password) ? $params->password : ''; + $port = isset($params->port) ? $params->port : null; + $ssl = isset($params->ssl) ? $params->ssl : false; + + $this->_protocol = new Zend_Mail_Protocol_Pop3(); + $this->_protocol->connect($host, $port, $ssl); + $this->_protocol->login($params->user, $password); + } + + /** + * Close resource for mail lib. If you need to control, when the resource + * is closed. Otherwise the destructor would call this. + * + * @return null + */ + public function close() + { + $this->_protocol->logout(); + } + + /** + * Keep the server busy. + * + * @return null + * @throws Zend_Mail_Protocol_Exception + */ + public function noop() + { + return $this->_protocol->noop(); + } + + /** + * Remove a message from server. If you're doing that from a web enviroment + * you should be careful and use a uniqueid as parameter if possible to + * identify the message. + * + * @param int $id number of message + * @return null + * @throws Zend_Mail_Protocol_Exception + */ + public function removeMessage($id) + { + $this->_protocol->delete($id); + } + + /** + * get unique id for one or all messages + * + * if storage does not support unique ids it's the same as the message number + * + * @param int|null $id message number + * @return array|string message number for given message or all messages as array + * @throws Zend_Mail_Storage_Exception + */ + public function getUniqueId($id = null) + { + if (!$this->hasUniqueid) { + if ($id) { + return $id; + } + $count = $this->countMessages(); + if ($count < 1) { + return array(); + } + $range = range(1, $count); + return array_combine($range, $range); + } + + return $this->_protocol->uniqueid($id); + } + + /** + * get a message number from a unique id + * + * I.e. if you have a webmailer that supports deleting messages you should use unique ids + * as parameter and use this method to translate it to message number right before calling removeMessage() + * + * @param string $id unique id + * @return int message number + * @throws Zend_Mail_Storage_Exception + */ + public function getNumberByUniqueId($id) + { + if (!$this->hasUniqueid) { + return $id; + } + + $ids = $this->getUniqueId(); + foreach ($ids as $k => $v) { + if ($v == $id) { + return $k; + } + } + + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('unique id not found'); + } + + /** + * Special handling for hasTop and hasUniqueid. The headers of the first message is + * retrieved if Top wasn't needed/tried yet. + * + * @see Zend_Mail_Storage_Abstract:__get() + * @param string $var + * @return string + * @throws Zend_Mail_Storage_Exception + */ + public function __get($var) + { + $result = parent::__get($var); + if ($result !== null) { + return $result; + } + + if (strtolower($var) == 'hastop') { + if ($this->_protocol->hasTop === null) { + // need to make a real call, because not all server are honest in their capas + try { + $this->_protocol->top(1, 0, false); + } catch(Zend_Mail_Exception $e) { + // ignoring error + } + } + $this->_has['top'] = $this->_protocol->hasTop; + return $this->_protocol->hasTop; + } + + if (strtolower($var) == 'hasuniqueid') { + $id = null; + try { + $id = $this->_protocol->uniqueid(1); + } catch(Zend_Mail_Exception $e) { + // ignoring error + } + $this->_has['uniqueid'] = $id ? true : false; + return $this->_has['uniqueid']; + } + + return $result; + } +} diff --git a/lib/Zend/Mail/Storage/Writable/Interface.php b/lib/Zend/Mail/Storage/Writable/Interface.php new file mode 100644 index 0000000..b49fc8e --- /dev/null +++ b/lib/Zend/Mail/Storage/Writable/Interface.php @@ -0,0 +1,108 @@ +create) && isset($params->dirname) && !file_exists($params->dirname . DIRECTORY_SEPARATOR . 'cur')) { + self::initMaildir($params->dirname); + } + + parent::__construct($params); + } + + /** + * create a new folder + * + * This method also creates parent folders if necessary. Some mail storages may restrict, which folder + * may be used as parent or which chars may be used in the folder name + * + * @param string $name global name of folder, local name if $parentFolder is set + * @param string|Zend_Mail_Storage_Folder $parentFolder parent folder for new folder, else root folder is parent + * @return string only used internally (new created maildir) + * @throws Zend_Mail_Storage_Exception + */ + public function createFolder($name, $parentFolder = null) + { + if ($parentFolder instanceof Zend_Mail_Storage_Folder) { + $folder = $parentFolder->getGlobalName() . $this->_delim . $name; + } else if ($parentFolder != null) { + $folder = rtrim($parentFolder, $this->_delim) . $this->_delim . $name; + } else { + $folder = $name; + } + + $folder = trim($folder, $this->_delim); + + // first we check if we try to create a folder that does exist + $exists = null; + try { + $exists = $this->getFolders($folder); + } catch (Zend_Mail_Exception $e) { + // ok + } + if ($exists) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('folder already exists'); + } + + if (strpos($folder, $this->_delim . $this->_delim) !== false) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('invalid name - folder parts may not be empty'); + } + + if (strpos($folder, 'INBOX' . $this->_delim) === 0) { + $folder = substr($folder, 6); + } + + $fulldir = $this->_rootdir . '.' . $folder; + + // check if we got tricked and would create a dir outside of the rootdir or not as direct child + if (strpos($folder, DIRECTORY_SEPARATOR) !== false || strpos($folder, '/') !== false + || dirname($fulldir) . DIRECTORY_SEPARATOR != $this->_rootdir) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('invalid name - no directory seprator allowed in folder name'); + } + + // has a parent folder? + $parent = null; + if (strpos($folder, $this->_delim)) { + // let's see if the parent folder exists + $parent = substr($folder, 0, strrpos($folder, $this->_delim)); + try { + $this->getFolders($parent); + } catch (Zend_Mail_Exception $e) { + // does not - create parent folder + $this->createFolder($parent); + } + } + + if (!@mkdir($fulldir) || !@mkdir($fulldir . DIRECTORY_SEPARATOR . 'cur')) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('error while creating new folder, may be created incompletly'); + } + + mkdir($fulldir . DIRECTORY_SEPARATOR . 'new'); + mkdir($fulldir . DIRECTORY_SEPARATOR . 'tmp'); + + $localName = $parent ? substr($folder, strlen($parent) + 1) : $folder; + $this->getFolders($parent)->$localName = new Zend_Mail_Storage_Folder($localName, $folder, true); + + return $fulldir; + } + + /** + * remove a folder + * + * @param string|Zend_Mail_Storage_Folder $name name or instance of folder + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function removeFolder($name) + { + // TODO: This could fail in the middle of the task, which is not optimal. + // But there is no defined standard way to mark a folder as removed and there is no atomar fs-op + // to remove a directory. Also moving the folder to a/the trash folder is not possible, as + // all parent folders must be created. What we could do is add a dash to the front of the + // directory name and it should be ignored as long as other processes obey the standard. + + if ($name instanceof Zend_Mail_Storage_Folder) { + $name = $name->getGlobalName(); + } + + $name = trim($name, $this->_delim); + if (strpos($name, 'INBOX' . $this->_delim) === 0) { + $name = substr($name, 6); + } + + // check if folder exists and has no children + if (!$this->getFolders($name)->isLeaf()) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('delete children first'); + } + + if ($name == 'INBOX' || $name == DIRECTORY_SEPARATOR || $name == '/') { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('wont delete INBOX'); + } + + if ($name == $this->getCurrentFolder()) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('wont delete selected folder'); + } + + foreach (array('tmp', 'new', 'cur', '.') as $subdir) { + $dir = $this->_rootdir . '.' . $name . DIRECTORY_SEPARATOR . $subdir; + if (!file_exists($dir)) { + continue; + } + $dh = opendir($dir); + if (!$dh) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("error opening $subdir"); + } + while (($entry = readdir($dh)) !== false) { + if ($entry == '.' || $entry == '..') { + continue; + } + if (!unlink($dir . DIRECTORY_SEPARATOR . $entry)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("error cleaning $subdir"); + } + } + closedir($dh); + if ($subdir !== '.') { + if (!rmdir($dir)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("error removing $subdir"); + } + } + } + + if (!rmdir($this->_rootdir . '.' . $name)) { + // at least we should try to make it a valid maildir again + mkdir($this->_rootdir . '.' . $name . DIRECTORY_SEPARATOR . 'cur'); + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("error removing maindir"); + } + + $parent = strpos($name, $this->_delim) ? substr($name, 0, strrpos($name, $this->_delim)) : null; + $localName = $parent ? substr($name, strlen($parent) + 1) : $name; + unset($this->getFolders($parent)->$localName); + } + + /** + * rename and/or move folder + * + * The new name has the same restrictions as in createFolder() + * + * @param string|Zend_Mail_Storage_Folder $oldName name or instance of folder + * @param string $newName new global name of folder + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function renameFolder($oldName, $newName) + { + // TODO: This is also not atomar and has similar problems as removeFolder() + + if ($oldName instanceof Zend_Mail_Storage_Folder) { + $oldName = $oldName->getGlobalName(); + } + + $oldName = trim($oldName, $this->_delim); + if (strpos($oldName, 'INBOX' . $this->_delim) === 0) { + $oldName = substr($oldName, 6); + } + + $newName = trim($newName, $this->_delim); + if (strpos($newName, 'INBOX' . $this->_delim) === 0) { + $newName = substr($newName, 6); + } + + if (strpos($newName, $oldName . $this->_delim) === 0) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('new folder cannot be a child of old folder'); + } + + // check if folder exists and has no children + $folder = $this->getFolders($oldName); + + if ($oldName == 'INBOX' || $oldName == DIRECTORY_SEPARATOR || $oldName == '/') { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('wont rename INBOX'); + } + + if ($oldName == $this->getCurrentFolder()) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('wont rename selected folder'); + } + + $newdir = $this->createFolder($newName); + + if (!$folder->isLeaf()) { + foreach ($folder as $k => $v) { + $this->renameFolder($v->getGlobalName(), $newName . $this->_delim . $k); + } + } + + $olddir = $this->_rootdir . '.' . $folder; + foreach (array('tmp', 'new', 'cur') as $subdir) { + $subdir = DIRECTORY_SEPARATOR . $subdir; + if (!file_exists($olddir . $subdir)) { + continue; + } + // using copy or moving files would be even better - but also much slower + if (!rename($olddir . $subdir, $newdir . $subdir)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('error while moving ' . $subdir); + } + } + // create a dummy if removing fails - otherwise we can't read it next time + mkdir($olddir . DIRECTORY_SEPARATOR . 'cur'); + $this->removeFolder($oldName); + } + + /** + * create a uniqueid for maildir filename + * + * This is nearly the format defined in the maildir standard. The microtime() call should already + * create a uniqueid, the pid is for multicore/-cpu machine that manage to call this function at the + * exact same time, and uname() gives us the hostname for multiple machines accessing the same storage. + * + * If someone disables posix we create a random number of the same size, so this method should also + * work on Windows - if you manage to get maildir working on Windows. + * Microtime could also be disabled, altough I've never seen it. + * + * @return string new uniqueid + */ + protected function _createUniqueId() + { + $id = ''; + $id .= function_exists('microtime') ? microtime(true) : (time() . ' ' . rand(0, 100000)); + $id .= '.' . (function_exists('posix_getpid') ? posix_getpid() : rand(50, 65535)); + $id .= '.' . php_uname('n'); + + return $id; + } + + /** + * open a temporary maildir file + * + * makes sure tmp/ exists and create a file with a unique name + * you should close the returned filehandle! + * + * @param string $folder name of current folder without leading . + * @return array array('dirname' => dir of maildir folder, 'uniq' => unique id, 'filename' => name of create file + * 'handle' => file opened for writing) + * @throws Zend_Mail_Storage_Exception + */ + protected function _createTmpFile($folder = 'INBOX') + { + if ($folder == 'INBOX') { + $tmpdir = $this->_rootdir . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR; + } else { + $tmpdir = $this->_rootdir . '.' . $folder . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR; + } + if (!file_exists($tmpdir)) { + if (!mkdir($tmpdir)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('problems creating tmp dir'); + } + } + + // we should retry to create a unique id if a file with the same name exists + // to avoid a script timeout we only wait 1 second (instead of 2) and stop + // after a defined retry count + // if you change this variable take into account that it can take up to $max_tries seconds + // normally we should have a valid unique name after the first try, we're just following the "standard" here + $max_tries = 5; + for ($i = 0; $i < $max_tries; ++$i) { + $uniq = $this->_createUniqueId(); + if (!file_exists($tmpdir . $uniq)) { + // here is the race condition! - as defined in the standard + // to avoid having a long time between stat()ing the file and creating it we're opening it here + // to mark the filename as taken + $fh = fopen($tmpdir . $uniq, 'w'); + if (!$fh) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('could not open temp file'); + } + break; + } + sleep(1); + } + + if (!$fh) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception("tried $max_tries unique ids for a temp file, but all were taken" + . ' - giving up'); + } + + return array('dirname' => $this->_rootdir . '.' . $folder, 'uniq' => $uniq, 'filename' => $tmpdir . $uniq, + 'handle' => $fh); + } + + /** + * create an info string for filenames with given flags + * + * @param array $flags wanted flags, with the reference you'll get the set flags with correct key (= char for flag) + * @return string info string for version 2 filenames including the leading colon + * @throws Zend_Mail_Storage_Exception + */ + protected function _getInfoString(&$flags) + { + // accessing keys is easier, faster and it removes duplicated flags + $wanted_flags = array_flip($flags); + if (isset($wanted_flags[Zend_Mail_Storage::FLAG_RECENT])) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('recent flag may not be set'); + } + + $info = ':2,'; + $flags = array(); + foreach (Zend_Mail_Storage_Maildir::$_knownFlags as $char => $flag) { + if (!isset($wanted_flags[$flag])) { + continue; + } + $info .= $char; + $flags[$char] = $flag; + unset($wanted_flags[$flag]); + } + + if (!empty($wanted_flags)) { + $wanted_flags = implode(', ', array_keys($wanted_flags)); + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('unknown flag(s): ' . $wanted_flags); + } + + return $info; + } + + /** + * append a new message to mail storage + * + * @param string|stream $message message as string or stream resource + * @param null|string|Zend_Mail_Storage_Folder $folder folder for new message, else current folder is taken + * @param null|array $flags set flags for new message, else a default set is used + * @param bool $recent handle this mail as if recent flag has been set, + * should only be used in delivery + * @throws Zend_Mail_Storage_Exception + */ + // not yet * @param string|Zend_Mail_Message|Zend_Mime_Message $message message as string or instance of message class + + public function appendMessage($message, $folder = null, $flags = null, $recent = false) + { + if ($this->_quota && $this->checkQuota()) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('storage is over quota!'); + } + + if ($folder === null) { + $folder = $this->_currentFolder; + } + + if (!($folder instanceof Zend_Mail_Storage_Folder)) { + $folder = $this->getFolders($folder); + } + + if ($flags === null) { + $flags = array(Zend_Mail_Storage::FLAG_SEEN); + } + $info = $this->_getInfoString($flags); + $temp_file = $this->_createTmpFile($folder->getGlobalName()); + + // TODO: handle class instances for $message + if (is_resource($message) && get_resource_type($message) == 'stream') { + stream_copy_to_stream($message, $temp_file['handle']); + } else { + fputs($temp_file['handle'], $message); + } + fclose($temp_file['handle']); + + // we're adding the size to the filename for maildir++ + $size = filesize($temp_file['filename']); + if ($size !== false) { + $info = ',S=' . $size . $info; + } + $new_filename = $temp_file['dirname'] . DIRECTORY_SEPARATOR; + $new_filename .= $recent ? 'new' : 'cur'; + $new_filename .= DIRECTORY_SEPARATOR . $temp_file['uniq'] . $info; + + // we're throwing any exception after removing our temp file and saving it to this variable instead + $exception = null; + + if (!link($temp_file['filename'], $new_filename)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + $exception = new Zend_Mail_Storage_Exception('cannot link message file to final dir'); + } + @unlink($temp_file['filename']); + + if ($exception) { + throw $exception; + } + + $this->_files[] = array('uniq' => $temp_file['uniq'], + 'flags' => $flags, + 'filename' => $new_filename); + if ($this->_quota) { + $this->_addQuotaEntry((int)$size, 1); + } + } + + /** + * copy an existing message + * + * @param int $id number of message + * @param string|Zend_Mail_Storage_Folder $folder name or instance of targer folder + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function copyMessage($id, $folder) + { + if ($this->_quota && $this->checkQuota()) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('storage is over quota!'); + } + + if (!($folder instanceof Zend_Mail_Storage_Folder)) { + $folder = $this->getFolders($folder); + } + + $filedata = $this->_getFileData($id); + $old_file = $filedata['filename']; + $flags = $filedata['flags']; + + // copied message can't be recent + while (($key = array_search(Zend_Mail_Storage::FLAG_RECENT, $flags)) !== false) { + unset($flags[$key]); + } + $info = $this->_getInfoString($flags); + + // we're creating the copy as temp file before moving to cur/ + $temp_file = $this->_createTmpFile($folder->getGlobalName()); + // we don't write directly to the file + fclose($temp_file['handle']); + + // we're adding the size to the filename for maildir++ + $size = filesize($old_file); + if ($size !== false) { + $info = ',S=' . $size . $info; + } + + $new_file = $temp_file['dirname'] . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . $temp_file['uniq'] . $info; + + // we're throwing any exception after removing our temp file and saving it to this variable instead + $exception = null; + + if (!copy($old_file, $temp_file['filename'])) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + $exception = new Zend_Mail_Storage_Exception('cannot copy message file'); + } else if (!link($temp_file['filename'], $new_file)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + $exception = new Zend_Mail_Storage_Exception('cannot link message file to final dir'); + } + @unlink($temp_file['filename']); + + if ($exception) { + throw $exception; + } + + if ($folder->getGlobalName() == $this->_currentFolder + || ($this->_currentFolder == 'INBOX' && $folder->getGlobalName() == '/')) { + $this->_files[] = array('uniq' => $temp_file['uniq'], + 'flags' => $flags, + 'filename' => $new_file); + } + + if ($this->_quota) { + $this->_addQuotaEntry((int)$size, 1); + } + } + + /** + * move an existing message + * + * @param int $id number of message + * @param string|Zend_Mail_Storage_Folder $folder name or instance of targer folder + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function moveMessage($id, $folder) { + if (!($folder instanceof Zend_Mail_Storage_Folder)) { + $folder = $this->getFolders($folder); + } + + if ($folder->getGlobalName() == $this->_currentFolder + || ($this->_currentFolder == 'INBOX' && $folder->getGlobalName() == '/')) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('target is current folder'); + } + + $filedata = $this->_getFileData($id); + $old_file = $filedata['filename']; + $flags = $filedata['flags']; + + // moved message can't be recent + while (($key = array_search(Zend_Mail_Storage::FLAG_RECENT, $flags)) !== false) { + unset($flags[$key]); + } + $info = $this->_getInfoString($flags); + + // reserving a new name + $temp_file = $this->_createTmpFile($folder->getGlobalName()); + fclose($temp_file['handle']); + + // we're adding the size to the filename for maildir++ + $size = filesize($old_file); + if ($size !== false) { + $info = ',S=' . $size . $info; + } + + $new_file = $temp_file['dirname'] . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . $temp_file['uniq'] . $info; + + // we're throwing any exception after removing our temp file and saving it to this variable instead + $exception = null; + + if (!rename($old_file, $new_file)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + $exception = new Zend_Mail_Storage_Exception('cannot move message file'); + } + @unlink($temp_file['filename']); + + if ($exception) { + throw $exception; + } + + unset($this->_files[$id - 1]); + // remove the gap + $this->_files = array_values($this->_files); + } + + + /** + * set flags for message + * + * NOTE: this method can't set the recent flag. + * + * @param int $id number of message + * @param array $flags new flags for message + * @throws Zend_Mail_Storage_Exception + */ + public function setFlags($id, $flags) + { + $info = $this->_getInfoString($flags); + $filedata = $this->_getFileData($id); + + // NOTE: double dirname to make sure we always move to cur. if recent flag has been set (message is in new) it will be moved to cur. + $new_filename = dirname(dirname($filedata['filename'])) . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . "$filedata[uniq]$info"; + + if (!@rename($filedata['filename'], $new_filename)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot rename file'); + } + + $filedata['flags'] = $flags; + $filedata['filename'] = $new_filename; + + $this->_files[$id - 1] = $filedata; + } + + + /** + * stub for not supported message deletion + * + * @return null + * @throws Zend_Mail_Storage_Exception + */ + public function removeMessage($id) + { + $filename = $this->_getFileData($id, 'filename'); + + if ($this->_quota) { + $size = filesize($filename); + } + + if (!@unlink($filename)) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot remove message'); + } + unset($this->_files[$id - 1]); + // remove the gap + $this->_files = array_values($this->_files); + if ($this->_quota) { + $this->_addQuotaEntry(0 - (int)$size, -1); + } + } + + /** + * enable/disable quota and set a quota value if wanted or needed + * + * You can enable/disable quota with true/false. If you don't have + * a MDA or want to enforce a quota value you can also set this value + * here. Use array('size' => SIZE_QUOTA, 'count' => MAX_MESSAGE) do + * define your quota. Order of these fields does matter! + * + * @param bool|array $value new quota value + * @return null + */ + public function setQuota($value) { + $this->_quota = $value; + } + + /** + * get currently set quota + * + * @see Zend_Mail_Storage_Writable_Maildir::setQuota() + * + * @return bool|array + */ + public function getQuota($fromStorage = false) { + if ($fromStorage) { + $fh = @fopen($this->_rootdir . 'maildirsize', 'r'); + if (!$fh) { + /** + * @see Zend_Mail_Storage_Exception + */ + require_once 'Zend/Mail/Storage/Exception.php'; + throw new Zend_Mail_Storage_Exception('cannot open maildirsize'); + } + $definition = fgets($fh); + fclose($fh); + $definition = explode(',', trim($definition)); + $quota = array(); + foreach ($definition as $member) { + $key = $member[strlen($member) - 1]; + if ($key == 'S' || $key == 'C') { + $key = $key == 'C' ? 'count' : 'size'; + } + $quota[$key] = substr($member, 0, -1); + } + return $quota; + } + + return $this->_quota; + } + + /** + * @see http://www.inter7.com/courierimap/README.maildirquota.html "Calculating maildirsize" + */ + protected function _calculateMaildirsize() { + $timestamps = array(); + $messages = 0; + $total_size = 0; + + if (is_array($this->_quota)) { + $quota = $this->_quota; + } else { + try { + $quota = $this->getQuota(true); + } catch (Zend_Mail_Storage_Exception $e) { + throw new Zend_Mail_Storage_Exception('no quota defintion found'); + } + } + + $folders = new RecursiveIteratorIterator($this->getFolders(), RecursiveIteratorIterator::SELF_FIRST); + foreach ($folders as $folder) { + $subdir = $folder->getGlobalName(); + if ($subdir == 'INBOX') { + $subdir = ''; + } else { + $subdir = '.' . $subdir; + } + if ($subdir == 'Trash') { + continue; + } + + foreach (array('cur', 'new') as $subsubdir) { + $dirname = $this->_rootdir . $subdir . DIRECTORY_SEPARATOR . $subsubdir . DIRECTORY_SEPARATOR; + if (!file_exists($dirname)) { + continue; + } + // NOTE: we are using mtime instead of "the latest timestamp". The latest would be atime + // and as we are accessing the directory it would make the whole calculation useless. + $timestamps[$dirname] = filemtime($dirname); + + $dh = opendir($dirname); + // NOTE: Should have been checked in constructor. Not throwing an exception here, quotas will + // therefore not be fully enforeced, but next request will fail anyway, if problem persists. + if (!$dh) { + continue; + } + + + while (($entry = readdir()) !== false) { + if ($entry[0] == '.' || !is_file($dirname . $entry)) { + continue; + } + + if (strpos($entry, ',S=')) { + strtok($entry, '='); + $filesize = strtok(':'); + if (is_numeric($filesize)) { + $total_size += $filesize; + ++$messages; + continue; + } + } + $size = filesize($dirname . $entry); + if ($size === false) { + // ignore, as we assume file got removed + continue; + } + $total_size += $size; + ++$messages; + } + } + } + + $tmp = $this->_createTmpFile(); + $fh = $tmp['handle']; + $definition = array(); + foreach ($quota as $type => $value) { + if ($type == 'size' || $type == 'count') { + $type = $type == 'count' ? 'C' : 'S'; + } + $definition[] = $value . $type; + } + $definition = implode(',', $definition); + fputs($fh, "$definition\n"); + fputs($fh, "$total_size $messages\n"); + fclose($fh); + rename($tmp['filename'], $this->_rootdir . 'maildirsize'); + foreach ($timestamps as $dir => $timestamp) { + if ($timestamp < filemtime($dir)) { + unlink($this->_rootdir . 'maildirsize'); + break; + } + } + + return array('size' => $total_size, 'count' => $messages, 'quota' => $quota); + } + + /** + * @see http://www.inter7.com/courierimap/README.maildirquota.html "Calculating the quota for a Maildir++" + */ + protected function _calculateQuota($forceRecalc = false) { + $fh = null; + $total_size = 0; + $messages = 0; + $maildirsize = ''; + if (!$forceRecalc && file_exists($this->_rootdir . 'maildirsize') && filesize($this->_rootdir . 'maildirsize') < 5120) { + $fh = fopen($this->_rootdir . 'maildirsize', 'r'); + } + if ($fh) { + $maildirsize = fread($fh, 5120); + if (strlen($maildirsize) >= 5120) { + fclose($fh); + $fh = null; + $maildirsize = ''; + } + } + if (!$fh) { + $result = $this->_calculateMaildirsize(); + $total_size = $result['size']; + $messages = $result['count']; + $quota = $result['quota']; + } else { + $maildirsize = explode("\n", $maildirsize); + if (is_array($this->_quota)) { + $quota = $this->_quota; + } else { + $definition = explode(',', $maildirsize[0]); + $quota = array(); + foreach ($definition as $member) { + $key = $member[strlen($member) - 1]; + if ($key == 'S' || $key == 'C') { + $key = $key == 'C' ? 'count' : 'size'; + } + $quota[$key] = substr($member, 0, -1); + } + } + unset($maildirsize[0]); + foreach ($maildirsize as $line) { + list($size, $count) = explode(' ', trim($line)); + $total_size += $size; + $messages += $count; + } + } + + $over_quota = false; + $over_quota = $over_quota || (isset($quota['size']) && $total_size > $quota['size']); + $over_quota = $over_quota || (isset($quota['count']) && $messages > $quota['count']); + // NOTE: $maildirsize equals false if it wasn't set (AKA we recalculated) or it's only + // one line, because $maildirsize[0] gets unsetted. + // Also we're using local time to calculate the 15 minute offset. Touching a file just for known the + // local time of the file storage isn't worth the hassle. + if ($over_quota && ($maildirsize || filemtime($this->_rootdir . 'maildirsize') > time() - 900)) { + $result = $this->_calculateMaildirsize(); + $total_size = $result['size']; + $messages = $result['count']; + $quota = $result['quota']; + $over_quota = false; + $over_quota = $over_quota || (isset($quota['size']) && $total_size > $quota['size']); + $over_quota = $over_quota || (isset($quota['count']) && $messages > $quota['count']); + } + + if ($fh) { + // TODO is there a safe way to keep the handle open for writing? + fclose($fh); + } + + return array('size' => $total_size, 'count' => $messages, 'quota' => $quota, 'over_quota' => $over_quota); + } + + protected function _addQuotaEntry($size, $count = 1) { + if (!file_exists($this->_rootdir . 'maildirsize')) { + // TODO: should get file handler from _calculateQuota + } + $size = (int)$size; + $count = (int)$count; + file_put_contents($this->_rootdir . 'maildirsize', "$size $count\n", FILE_APPEND); + } + + /** + * check if storage is currently over quota + * + * @param bool $detailedResponse return known data of quota and current size and message count @see _calculateQuota() + * @return bool|array over quota state or detailed response + */ + public function checkQuota($detailedResponse = false, $forceRecalc = false) { + $result = $this->_calculateQuota($forceRecalc); + return $detailedResponse ? $result : $result['over_quota']; + } +} diff --git a/lib/Zend/Mail/Transport/Abstract.php b/lib/Zend/Mail/Transport/Abstract.php new file mode 100644 index 0000000..603411a --- /dev/null +++ b/lib/Zend/Mail/Transport/Abstract.php @@ -0,0 +1,350 @@ +_mail->getType(); + if (!$type) { + if ($this->_mail->hasAttachments) { + $type = Zend_Mime::MULTIPART_MIXED; + } elseif ($this->_mail->getBodyText() && $this->_mail->getBodyHtml()) { + $type = Zend_Mime::MULTIPART_ALTERNATIVE; + } else { + $type = Zend_Mime::MULTIPART_MIXED; + } + } + + $this->_headers['Content-Type'] = array( + $type . '; charset="' . $this->_mail->getCharset() . '";' + . $this->EOL + . " " . 'boundary="' . $boundary . '"' + ); + $this->_headers['MIME-Version'] = array('1.0'); + + $this->boundary = $boundary; + } + + return $this->_headers; + } + + /** + * Prepend header name to header value + * + * @param string $item + * @param string $key + * @param string $prefix + * @static + * @access protected + * @return void + */ + protected static function _formatHeader(&$item, $key, $prefix) + { + $item = $prefix . ': ' . $item; + } + + /** + * Prepare header string for use in transport + * + * Prepares and generates {@link $header} based on the headers provided. + * + * @param mixed $headers + * @access protected + * @return void + * @throws Zend_Mail_Transport_Exception if any header lines exceed 998 + * characters + */ + protected function _prepareHeaders($headers) + { + if (!$this->_mail) { + /** + * @see Zend_Mail_Transport_Exception + */ + require_once 'Zend/Mail/Transport/Exception.php'; + throw new Zend_Mail_Transport_Exception('Missing Zend_Mail object in _mail property'); + } + + $this->header = ''; + + foreach ($headers as $header => $content) { + if (isset($content['append'])) { + unset($content['append']); + $value = implode(',' . $this->EOL . ' ', $content); + $this->header .= $header . ': ' . $value . $this->EOL; + } else { + array_walk($content, array(get_class($this), '_formatHeader'), $header); + $this->header .= implode($this->EOL, $content) . $this->EOL; + } + } + + // Sanity check on headers -- should not be > 998 characters + $sane = true; + foreach (explode($this->EOL, $this->header) as $line) { + if (strlen(trim($line)) > 998) { + $sane = false; + break; + } + } + if (!$sane) { + /** + * @see Zend_Mail_Transport_Exception + */ + require_once 'Zend/Mail/Transport/Exception.php'; + throw new Zend_Mail_Exception('At least one mail header line is too long'); + } + } + + /** + * Generate MIME compliant message from the current configuration + * + * If both a text and HTML body are present, generates a + * multipart/alternative Zend_Mime_Part containing the headers and contents + * of each. Otherwise, uses whichever of the text or HTML parts present. + * + * The content part is then prepended to the list of Zend_Mime_Parts for + * this message. + * + * @return void + */ + protected function _buildBody() + { + if (($text = $this->_mail->getBodyText()) + && ($html = $this->_mail->getBodyHtml())) + { + // Generate unique boundary for multipart/alternative + $mime = new Zend_Mime(null); + $boundaryLine = $mime->boundaryLine($this->EOL); + $boundaryEnd = $mime->mimeEnd($this->EOL); + + $text->disposition = false; + $html->disposition = false; + + $body = $boundaryLine + . $text->getHeaders($this->EOL) + . $this->EOL + . $text->getContent($this->EOL) + . $this->EOL + . $boundaryLine + . $html->getHeaders($this->EOL) + . $this->EOL + . $html->getContent($this->EOL) + . $this->EOL + . $boundaryEnd; + + $mp = new Zend_Mime_Part($body); + $mp->type = Zend_Mime::MULTIPART_ALTERNATIVE; + $mp->boundary = $mime->boundary(); + + $this->_isMultipart = true; + + // Ensure first part contains text alternatives + array_unshift($this->_parts, $mp); + + // Get headers + $this->_headers = $this->_mail->getHeaders(); + return; + } + + // If not multipart, then get the body + if (false !== ($body = $this->_mail->getBodyHtml())) { + array_unshift($this->_parts, $body); + } elseif (false !== ($body = $this->_mail->getBodyText())) { + array_unshift($this->_parts, $body); + } + + if (!$body) { + /** + * @see Zend_Mail_Transport_Exception + */ + require_once 'Zend/Mail/Transport/Exception.php'; + throw new Zend_Mail_Transport_Exception('No body specified'); + } + + // Get headers + $this->_headers = $this->_mail->getHeaders(); + $headers = $body->getHeadersArray($this->EOL); + foreach ($headers as $header) { + // Headers in Zend_Mime_Part are kept as arrays with two elements, a + // key and a value + $this->_headers[$header[0]] = array($header[1]); + } + } + + /** + * Send a mail using this transport + * + * @param Zend_Mail $mail + * @access public + * @return void + * @throws Zend_Mail_Transport_Exception if mail is empty + */ + public function send(Zend_Mail $mail) + { + $this->_isMultipart = false; + $this->_mail = $mail; + $this->_parts = $mail->getParts(); + $mime = $mail->getMime(); + + // Build body content + $this->_buildBody(); + + // Determine number of parts and boundary + $count = count($this->_parts); + $boundary = null; + if ($count < 1) { + /** + * @see Zend_Mail_Transport_Exception + */ + require_once 'Zend/Mail/Transport/Exception.php'; + throw new Zend_Mail_Transport_Exception('Empty mail cannot be sent'); + } + + if ($count > 1) { + // Multipart message; create new MIME object and boundary + $mime = new Zend_Mime($this->_mail->getMimeBoundary()); + $boundary = $mime->boundary(); + } elseif ($this->_isMultipart) { + // multipart/alternative -- grab boundary + $boundary = $this->_parts[0]->boundary; + } + + // Determine recipients, and prepare headers + $this->recipients = implode(',', $mail->getRecipients()); + $this->_prepareHeaders($this->_getHeaders($boundary)); + + // Create message body + // This is done so that the same Zend_Mail object can be used in + // multiple transports + $message = new Zend_Mime_Message(); + $message->setParts($this->_parts); + $message->setMime($mime); + $this->body = $message->generateMessage($this->EOL); + + // Send to transport! + $this->_sendMail(); + } +} diff --git a/lib/Zend/Mail/Transport/Exception.php b/lib/Zend/Mail/Transport/Exception.php new file mode 100644 index 0000000..16ef240 --- /dev/null +++ b/lib/Zend/Mail/Transport/Exception.php @@ -0,0 +1,39 @@ +parameters = $parameters; + } + + + /** + * Send mail using PHP native mail() + * + * @access public + * @return void + * @throws Zend_Mail_Transport_Exception on mail() failure + */ + public function _sendMail() + { + if ($this->parameters === null) { + $result = mail( + $this->recipients, + $this->_mail->getSubject(), + $this->body, + $this->header); + } else { + $result = mail( + $this->recipients, + $this->_mail->getSubject(), + $this->body, + $this->header, + $this->parameters); + } + if (!$result) { + /** + * @see Zend_Mail_Transport_Exception + */ + require_once 'Zend/Mail/Transport/Exception.php'; + throw new Zend_Mail_Transport_Exception('Unable to send mail'); + } + } + + + /** + * Format and fix headers + * + * mail() uses its $to and $subject arguments to set the To: and Subject: + * headers, respectively. This method strips those out as a sanity check to + * prevent duplicate header entries. + * + * @access protected + * @param array $headers + * @return void + * @throws Zend_Mail_Transport_Exception + */ + protected function _prepareHeaders($headers) + { + if (!$this->_mail) { + /** + * @see Zend_Mail_Transport_Exception + */ + require_once 'Zend/Mail/Transport/Exception.php'; + throw new Zend_Mail_Transport_Exception('_prepareHeaders requires a registered Zend_Mail object'); + } + + // mail() uses its $to parameter to set the To: header, and the $subject + // parameter to set the Subject: header. We need to strip them out. + if (0 === strpos(PHP_OS, 'WIN')) { + // If the current recipients list is empty, throw an error + if (empty($this->recipients)) { + /** + * @see Zend_Mail_Transport_Exception + */ + require_once 'Zend/Mail/Transport/Exception.php'; + throw new Zend_Mail_Transport_Exception('Missing To addresses'); + } + } else { + // All others, simply grab the recipients and unset the To: header + if (!isset($headers['To'])) { + /** + * @see Zend_Mail_Transport_Exception + */ + require_once 'Zend/Mail/Transport/Exception.php'; + throw new Zend_Mail_Transport_Exception('Missing To header'); + } + + unset($headers['To']['append']); + $this->recipients = implode(',', $headers['To']); + } + + // Remove recipient header + unset($headers['To']); + + // Remove subject header, if present + if (isset($headers['Subject'])) { + unset($headers['Subject']); + } + + // Prepare headers + parent::_prepareHeaders($headers); + } + +} + diff --git a/lib/Zend/Mail/Transport/Smtp.php b/lib/Zend/Mail/Transport/Smtp.php new file mode 100644 index 0000000..714266e --- /dev/null +++ b/lib/Zend/Mail/Transport/Smtp.php @@ -0,0 +1,241 @@ +_name = $config['name']; + } + if (isset($config['port'])) { + $this->_port = $config['port']; + } + if (isset($config['auth'])) { + $this->_auth = $config['auth']; + } + + $this->_host = $host; + $this->_config = $config; + } + + + /** + * Class destructor to ensure all open connections are closed + * + * @return void + */ + public function __destruct() + { + if ($this->_connection instanceof Zend_Mail_Protocol_Smtp) { + try { + $this->_connection->quit(); + } catch (Zend_Mail_Protocol_Exception $e) { + // ignore + } + $this->_connection->disconnect(); + } + } + + + /** + * Sets the connection protocol instance + * + * @param Zend_Mail_Protocol_Abstract $client + * + * @return void + */ + public function setConnection(Zend_Mail_Protocol_Abstract $connection) + { + $this->_connection = $connection; + } + + + /** + * Gets the connection protocol instance + * + * @return Zend_Mail_Protocol|null + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * Send an email via the SMTP connection protocol + * + * The connection via the protocol adapter is made just-in-time to allow a + * developer to add a custom adapter if required before mail is sent. + * + * @return void + */ + public function _sendMail() + { + // If sending multiple messages per session use existing adapter + if (!($this->_connection instanceof Zend_Mail_Protocol_Smtp)) { + // Check if authentication is required and determine required class + $connectionClass = 'Zend_Mail_Protocol_Smtp'; + if ($this->_auth) { + $connectionClass .= '_Auth_' . ucwords($this->_auth); + } + Zend_Loader::loadClass($connectionClass); + $this->setConnection(new $connectionClass($this->_host, $this->_port, $this->_config)); + $this->_connection->connect(); + $this->_connection->helo($this->_name); + } else { + // Reset connection to ensure reliable transaction + $this->_connection->rset(); + } + + // Set mail return path from sender email address + $this->_connection->mail($this->_mail->getReturnPath()); + + // Set recipient forward paths + foreach ($this->_mail->getRecipients() as $recipient) { + $this->_connection->rcpt($recipient); + } + + // Issue DATA command to client + $this->_connection->data($this->header . Zend_Mime::LINEEND . $this->body); + } + + /** + * Format and fix headers + * + * Some SMTP servers do not strip BCC headers. Most clients do it themselves as do we. + * + * @access protected + * @param array $headers + * @return void + * @throws Zend_Transport_Exception + */ + protected function _prepareHeaders($headers) + { + if (!$this->_mail) { + /** + * @see Zend_Mail_Transport_Exception + */ + require_once 'Zend/Mail/Transport/Exception.php'; + throw new Zend_Mail_Transport_Exception('_prepareHeaders requires a registered Zend_Mail object'); + } + + unset($headers['Bcc']); + + // Prepare headers + parent::_prepareHeaders($headers); + } +} diff --git a/lib/Zend/Mime.php b/lib/Zend/Mime.php new file mode 100644 index 0000000..414e541 --- /dev/null +++ b/lib/Zend/Mime.php @@ -0,0 +1,365 @@ + $lineLength) { + $ptr = $lineLength; + } + + // Ensure we are not splitting across an encoded character + $pos = strrpos(substr($str, 0, $ptr), '='); + if ($pos !== false && $pos >= $ptr - 2) { + $ptr = $pos; + } + + // Check if there is a space at the end of the line and rewind + if ($ptr > 0 && $str[$ptr - 1] == ' ') { + --$ptr; + } + + // Add string and continue + $out .= substr($str, 0, $ptr) . '=' . $lineEnd; + $str = substr($str, $ptr); + } + + $out = rtrim($out, $lineEnd); + $out = rtrim($out, '='); + return $out; + } + + /** + * Converts a string into quoted printable format. + * + * @param string $str + * @return string + */ + private static function _encodeQuotedPrintable($str) + { + $str = str_replace('=', '=3D', $str); + $str = str_replace(self::$qpKeys, self::$qpReplaceValues, $str); + $str = rtrim($str); + return $str; + } + + /** + * Encode a given string with the QUOTED_PRINTABLE mechanism for Mail Headers. + * + * Mail headers depend on an extended quoted printable algorithm otherwise + * a range of bugs can occur. + * + * @param string $str + * @param string $charset + * @param int $lineLength Defaults to {@link LINELENGTH} + * @param int $lineEnd Defaults to {@link LINEEND} + * @return string + */ + public static function encodeQuotedPrintableHeader($str, $charset, + $lineLength = self::LINELENGTH, + $lineEnd = self::LINEEND) + { + // Reduce line-length by the length of the required delimiter, charsets and encoding + $prefix = sprintf('=?%s?Q?', $charset); + $lineLength = $lineLength-strlen($prefix)-3; + + $str = self::_encodeQuotedPrintable($str); + + // Mail-Header required chars have to be encoded also: + $str = str_replace(array('?', ' ', '_'), array('=3F', '=20', '=5F'), $str); + + // initialize first line, we need it anyways + $lines = array(0 => ""); + + // Split encoded text into separate lines + $tmp = ""; + while(strlen($str) > 0) { + $currentLine = max(count($lines)-1, 0); + $token = self::getNextQuotedPrintableToken($str); + $str = substr($str, strlen($token)); + + $tmp .= $token; + if( (strlen($token) == 1 && strpbrk($token, '!%,.:;<>')) + || in_array($token, array("=3F", "=20", "=5F")) ) { + // only if we have a single char token or space, we can append the + // tempstring it to the current line or start a new line if necessary. + if(strlen($lines[$currentLine].$tmp) > $lineLength) { + $lines[$currentLine+1] = $tmp; + } else { + $lines[$currentLine] .= $tmp; + } + $tmp = ""; + } + // don't forget to append the rest to the last line + if(strlen($str) == 0) { + $lines[$currentLine] .= $tmp; + } + } + + // assemble the lines together by pre- and appending delimiters, charset, encoding. + for($i = 0; $i < count($lines); $i++) { + $lines[$i] = " ".$prefix.$lines[$i]."?="; + } + $str = trim(implode($lineEnd, $lines)); + return $str; + } + + /** + * Retrieves the first token from a quoted printable string. + * + * @param string $str + * @return string + */ + private static function getNextQuotedPrintableToken($str) + { + if(substr($str, 0, 1) == "=") { + $token = substr($str, 0, 3); + } else { + $token = substr($str, 0, 1); + } + return $token; + } + + /** + * Encode a given string in mail header compatible base64 encoding. + * + * @param string $str + * @param string $charset + * @param int $lineLength Defaults to {@link LINELENGTH} + * @param int $lineEnd Defaults to {@link LINEEND} + * @return string + */ + public static function encodeBase64Header($str, + $charset, + $lineLength = self::LINELENGTH, + $lineEnd = self::LINEEND) + { + $prefix = '=?' . $charset . '?B?'; + $suffix = '?='; + $remainingLength = $lineLength - strlen($prefix) - strlen($suffix); + + $encodedValue = self::encodeBase64($str, $remainingLength, $lineEnd); + $encodedValue = str_replace($lineEnd, $suffix . $lineEnd . ' ' . $prefix, $encodedValue); + $encodedValue = $prefix . $encodedValue . $suffix; + return $encodedValue; + } + + /** + * Encode a given string in base64 encoding and break lines + * according to the maximum linelength. + * + * @param string $str + * @param int $lineLength Defaults to {@link LINELENGTH} + * @param int $lineEnd Defaults to {@link LINEEND} + * @return string + */ + public static function encodeBase64($str, + $lineLength = self::LINELENGTH, + $lineEnd = self::LINEEND) + { + return rtrim(chunk_split(base64_encode($str), $lineLength, $lineEnd)); + } + + /** + * Constructor + * + * @param null|string $boundary + * @access public + * @return void + */ + public function __construct($boundary = null) + { + // This string needs to be somewhat unique + if ($boundary === null) { + $this->_boundary = '=_' . md5(microtime(1) . self::$makeUnique++); + } else { + $this->_boundary = $boundary; + } + } + + /** + * Encode the given string with the given encoding. + * + * @param string $str + * @param string $encoding + * @param string $EOL EOL string; defaults to {@link Zend_Mime::LINEEND} + * @return string + */ + public static function encode($str, $encoding, $EOL = self::LINEEND) + { + switch ($encoding) { + case self::ENCODING_BASE64: + return self::encodeBase64($str, self::LINELENGTH, $EOL); + + case self::ENCODING_QUOTEDPRINTABLE: + return self::encodeQuotedPrintable($str, self::LINELENGTH, $EOL); + + default: + /** + * @todo 7Bit and 8Bit is currently handled the same way. + */ + return $str; + } + } + + /** + * Return a MIME boundary + * + * @access public + * @return string + */ + public function boundary() + { + return $this->_boundary; + } + + /** + * Return a MIME boundary line + * + * @param mixed $EOL Defaults to {@link LINEEND} + * @access public + * @return string + */ + public function boundaryLine($EOL = self::LINEEND) + { + return $EOL . '--' . $this->_boundary . $EOL; + } + + /** + * Return MIME ending + * + * @access public + * @return string + */ + public function mimeEnd($EOL = self::LINEEND) + { + return $EOL . '--' . $this->_boundary . '--' . $EOL; + } +} diff --git a/lib/Zend/Mime/Decode.php b/lib/Zend/Mime/Decode.php new file mode 100644 index 0000000..cc4de8b --- /dev/null +++ b/lib/Zend/Mime/Decode.php @@ -0,0 +1,243 @@ + array(name => value), 'body' => content), null if no parts found + * @throws Zend_Exception + */ + public static function splitMessageStruct($message, $boundary, $EOL = Zend_Mime::LINEEND) + { + $parts = self::splitMime($message, $boundary); + if (count($parts) <= 0) { + return null; + } + $result = array(); + foreach ($parts as $part) { + self::splitMessage($part, $headers, $body, $EOL); + $result[] = array('header' => $headers, + 'body' => $body ); + } + return $result; + } + + /** + * split a message in header and body part, if no header or an + * invalid header is found $headers is empty + * + * The charset of the returned headers depend on your iconv settings. + * + * @param string $message raw message with header and optional content + * @param array $headers output param, array with headers as array(name => value) + * @param string $body output param, content of message + * @param string $EOL EOL string; defaults to {@link Zend_Mime::LINEEND} + * @return null + */ + public static function splitMessage($message, &$headers, &$body, $EOL = Zend_Mime::LINEEND) + { + // check for valid header at first line + $firstline = strtok($message, "\n"); + if (!preg_match('%^[^\s]+[^:]*:%', $firstline)) { + $headers = array(); + // TODO: we're ignoring \r for now - is this function fast enough and is it safe to asume noone needs \r? + $body = str_replace(array("\r", "\n"), array('', $EOL), $message); + return; + } + + // find an empty line between headers and body + // default is set new line + if (strpos($message, $EOL . $EOL)) { + list($headers, $body) = explode($EOL . $EOL, $message, 2); + // next is the standard new line + } else if ($EOL != "\r\n" && strpos($message, "\r\n\r\n")) { + list($headers, $body) = explode("\r\n\r\n", $message, 2); + // next is the other "standard" new line + } else if ($EOL != "\n" && strpos($message, "\n\n")) { + list($headers, $body) = explode("\n\n", $message, 2); + // at last resort find anything that looks like a new line + } else { + @list($headers, $body) = @preg_split("%([\r\n]+)\\1%U", $message, 2); + } + + $headers = iconv_mime_decode_headers($headers, ICONV_MIME_DECODE_CONTINUE_ON_ERROR); + + if ($headers === false ) { + // an error occurs during the decoding + return; + } + + // normalize header names + foreach ($headers as $name => $header) { + $lower = strtolower($name); + if ($lower == $name) { + continue; + } + unset($headers[$name]); + if (!isset($headers[$lower])) { + $headers[$lower] = $header; + continue; + } + if (is_array($headers[$lower])) { + $headers[$lower][] = $header; + continue; + } + $headers[$lower] = array($headers[$lower], $header); + } + } + + /** + * split a content type in its different parts + * + * @param string $type content-type + * @param string $wantedPart the wanted part, else an array with all parts is returned + * @return string|array wanted part or all parts as array('type' => content-type, partname => value) + */ + public static function splitContentType($type, $wantedPart = null) + { + return self::splitHeaderField($type, $wantedPart, 'type'); + } + + /** + * split a header field like content type in its different parts + * + * @param string $type header field + * @param string $wantedPart the wanted part, else an array with all parts is returned + * @param string $firstName key name for the first part + * @return string|array wanted part or all parts as array($firstName => firstPart, partname => value) + * @throws Zend_Exception + */ + public static function splitHeaderField($field, $wantedPart = null, $firstName = 0) + { + $wantedPart = strtolower($wantedPart); + $firstName = strtolower($firstName); + + // special case - a bit optimized + if ($firstName === $wantedPart) { + $field = strtok($field, ';'); + return $field[0] == '"' ? substr($field, 1, -1) : $field; + } + + $field = $firstName . '=' . $field; + if (!preg_match_all('%([^=\s]+)\s*=\s*("[^"]+"|[^;]+)(;\s*|$)%', $field, $matches)) { + throw new Zend_Exception('not a valid header field'); + } + + if ($wantedPart) { + foreach ($matches[1] as $key => $name) { + if (strcasecmp($name, $wantedPart)) { + continue; + } + if ($matches[2][$key][0] != '"') { + return $matches[2][$key]; + } + return substr($matches[2][$key], 1, -1); + } + return null; + } + + $split = array(); + foreach ($matches[1] as $key => $name) { + $name = strtolower($name); + if ($matches[2][$key][0] == '"') { + $split[$name] = substr($matches[2][$key], 1, -1); + } else { + $split[$name] = $matches[2][$key]; + } + } + + return $split; + } + + /** + * decode a quoted printable encoded string + * + * The charset of the returned string depends on your iconv settings. + * + * @param string encoded string + * @return string decoded string + */ + public static function decodeQuotedPrintable($string) + { + return iconv_mime_decode($string, ICONV_MIME_DECODE_CONTINUE_ON_ERROR); + } +} diff --git a/lib/Zend/Mime/Exception.php b/lib/Zend/Mime/Exception.php new file mode 100644 index 0000000..2c22e9b --- /dev/null +++ b/lib/Zend/Mime/Exception.php @@ -0,0 +1,36 @@ +_parts; + } + + /** + * Sets the given array of Zend_Mime_Parts as the array for the message + * + * @param array $parts + */ + public function setParts($parts) + { + $this->_parts = $parts; + } + + /** + * Append a new Zend_Mime_Part to the current message + * + * @param Zend_Mime_Part $part + */ + public function addPart(Zend_Mime_Part $part) + { + /** + * @todo check for duplicate object handle + */ + $this->_parts[] = $part; + } + + /** + * Check if message needs to be sent as multipart + * MIME message or if it has only one part. + * + * @return boolean + */ + public function isMultiPart() + { + return (count($this->_parts) > 1); + } + + /** + * Set Zend_Mime object for the message + * + * This can be used to set the boundary specifically or to use a subclass of + * Zend_Mime for generating the boundary. + * + * @param Zend_Mime $mime + */ + public function setMime(Zend_Mime $mime) + { + $this->_mime = $mime; + } + + /** + * Returns the Zend_Mime object in use by the message + * + * If the object was not present, it is created and returned. Can be used to + * determine the boundary used in this message. + * + * @return Zend_Mime + */ + public function getMime() + { + if ($this->_mime === null) { + $this->_mime = new Zend_Mime(); + } + + return $this->_mime; + } + + /** + * Generate MIME-compliant message from the current configuration + * + * This can be a multipart message if more than one MIME part was added. If + * only one part is present, the content of this part is returned. If no + * part had been added, an empty string is returned. + * + * Parts are seperated by the mime boundary as defined in Zend_Mime. If + * {@link setMime()} has been called before this method, the Zend_Mime + * object set by this call will be used. Otherwise, a new Zend_Mime object + * is generated and used. + * + * @param string $EOL EOL string; defaults to {@link Zend_Mime::LINEEND} + * @return string + */ + public function generateMessage($EOL = Zend_Mime::LINEEND) + { + if (! $this->isMultiPart()) { + $body = array_shift($this->_parts); + $body = $body->getContent($EOL); + } else { + $mime = $this->getMime(); + + $boundaryLine = $mime->boundaryLine($EOL); + $body = 'This is a message in Mime Format. If you see this, ' + . "your mail reader does not support this format." . $EOL; + + foreach (array_keys($this->_parts) as $p) { + $body .= $boundaryLine + . $this->getPartHeaders($p, $EOL) + . $EOL + . $this->getPartContent($p, $EOL); + } + + $body .= $mime->mimeEnd($EOL); + } + + return trim($body); + } + + /** + * Get the headers of a given part as an array + * + * @param int $partnum + * @return array + */ + public function getPartHeadersArray($partnum) + { + return $this->_parts[$partnum]->getHeadersArray(); + } + + /** + * Get the headers of a given part as a string + * + * @param int $partnum + * @return string + */ + public function getPartHeaders($partnum, $EOL = Zend_Mime::LINEEND) + { + return $this->_parts[$partnum]->getHeaders($EOL); + } + + /** + * Get the (encoded) content of a given part as a string + * + * @param int $partnum + * @return string + */ + public function getPartContent($partnum, $EOL = Zend_Mime::LINEEND) + { + return $this->_parts[$partnum]->getContent($EOL); + } + + /** + * Explode MIME multipart string into seperate parts + * + * Parts consist of the header and the body of each MIME part. + * + * @param string $body + * @param string $boundary + * @return array + */ + protected static function _disassembleMime($body, $boundary) + { + $start = 0; + $res = array(); + // find every mime part limiter and cut out the + // string before it. + // the part before the first boundary string is discarded: + $p = strpos($body, '--'.$boundary."\n", $start); + if ($p === false) { + // no parts found! + return array(); + } + + // position after first boundary line + $start = $p + 3 + strlen($boundary); + + while (($p = strpos($body, '--' . $boundary . "\n", $start)) !== false) { + $res[] = substr($body, $start, $p-$start); + $start = $p + 3 + strlen($boundary); + } + + // no more parts, find end boundary + $p = strpos($body, '--' . $boundary . '--', $start); + if ($p===false) { + throw new Zend_Exception('Not a valid Mime Message: End Missing'); + } + + // the remaining part also needs to be parsed: + $res[] = substr($body, $start, $p-$start); + return $res; + } + + /** + * Decodes a MIME encoded string and returns a Zend_Mime_Message object with + * all the MIME parts set according to the given string + * + * @param string $message + * @param string $boundary + * @param string $EOL EOL string; defaults to {@link Zend_Mime::LINEEND} + * @return Zend_Mime_Message + */ + public static function createFromMessage($message, $boundary, $EOL = Zend_Mime::LINEEND) + { + require_once 'Zend/Mime/Decode.php'; + $parts = Zend_Mime_Decode::splitMessageStruct($message, $boundary, $EOL); + + $res = new self(); + foreach ($parts as $part) { + // now we build a new MimePart for the current Message Part: + $newPart = new Zend_Mime_Part($part['body']); + foreach ($part['header'] as $key => $value) { + /** + * @todo check for characterset and filename + */ + switch(strtolower($key)) { + case 'content-type': + $newPart->type = $value; + break; + case 'content-transfer-encoding': + $newPart->encoding = $value; + break; + case 'content-id': + $newPart->id = trim($value,'<>'); + break; + case 'content-disposition': + $newPart->disposition = $value; + break; + case 'content-description': + $newPart->description = $value; + break; + case 'content-location': + $newPart->location = $value; + break; + case 'content-language': + $newPart->language = $value; + break; + default: + throw new Zend_Exception('Unknown header ignored for MimePart:' . $key); + } + } + $res->addPart($newPart); + } + return $res; + } +} diff --git a/lib/Zend/Mime/Part.php b/lib/Zend/Mime/Part.php new file mode 100644 index 0000000..a3e261b --- /dev/null +++ b/lib/Zend/Mime/Part.php @@ -0,0 +1,216 @@ +_content = $content; + if (is_resource($content)) { + $this->_isStream = true; + } + } + + /** + * @todo setters/getters + * @todo error checking for setting $type + * @todo error checking for setting $encoding + */ + + /** + * check if this part can be read as a stream. + * if true, getEncodedStream can be called, otherwise + * only getContent can be used to fetch the encoded + * content of the part + * + * @return bool + */ + public function isStream() + { + return $this->_isStream; + } + + /** + * if this was created with a stream, return a filtered stream for + * reading the content. very useful for large file attachments. + * + * @return stream + * @throws Zend_Mime_Exception if not a stream or unable to append filter + */ + public function getEncodedStream() + { + if (!$this->_isStream) { + require_once 'Zend/Mime/Exception.php'; + throw new Zend_Mime_Exception('Attempt to get a stream from a string part'); + } + + //stream_filter_remove(); // ??? is that right? + switch ($this->encoding) { + case Zend_Mime::ENCODING_QUOTEDPRINTABLE: + $filter = stream_filter_append( + $this->_content, + 'convert.quoted-printable-encode', + STREAM_FILTER_READ, + array( + 'line-length' => 76, + 'line-break-chars' => Zend_Mime::LINEEND + ) + ); + if (!is_resource($filter)) { + require_once 'Zend/Mime/Exception.php'; + throw new Zend_Mime_Exception('Failed to append quoted-printable filter'); + } + break; + case Zend_Mime::ENCODING_BASE64: + $filter = stream_filter_append( + $this->_content, + 'convert.base64-encode', + STREAM_FILTER_READ, + array( + 'line-length' => 76, + 'line-break-chars' => Zend_Mime::LINEEND + ) + ); + if (!is_resource($filter)) { + require_once 'Zend/Mime/Exception.php'; + throw new Zend_Mime_Exception('Failed to append base64 filter'); + } + break; + default: + } + return $this->_content; + } + + /** + * Get the Content of the current Mime Part in the given encoding. + * + * @return String + */ + public function getContent($EOL = Zend_Mime::LINEEND) + { + if ($this->_isStream) { + return stream_get_contents($this->getEncodedStream()); + } else { + return Zend_Mime::encode($this->_content, $this->encoding, $EOL); + } + } + + /** + * Create and return the array of headers for this MIME part + * + * @access public + * @return array + */ + public function getHeadersArray($EOL = Zend_Mime::LINEEND) + { + $headers = array(); + + $contentType = $this->type; + if ($this->charset) { + $contentType .= '; charset="' . $this->charset . '"'; + } + + if ($this->boundary) { + $contentType .= ';' . $EOL + . " boundary=\"" . $this->boundary . '"'; + } + + $headers[] = array('Content-Type', $contentType); + + if ($this->encoding) { + $headers[] = array('Content-Transfer-Encoding', $this->encoding); + } + + if ($this->id) { + $headers[] = array('Content-ID', '<' . $this->id . '>'); + } + + if ($this->disposition) { + $disposition = $this->disposition; + if ($this->filename) { + $disposition .= '; filename="' . $this->filename . '"'; + } + $headers[] = array('Content-Disposition', $disposition); + } + + if ($this->description) { + $headers[] = array('Content-Description', $this->description); + } + + if ($this->location) { + $headers[] = array('Content-Location', $this->location); + } + + if ($this->language){ + $headers[] = array('Content-Language', $this->language); + } + + return $headers; + } + + /** + * Return the headers for this part as a string + * + * @return String + */ + public function getHeaders($EOL = Zend_Mime::LINEEND) + { + $res = ''; + foreach ($this->getHeadersArray($EOL) as $header) { + $res .= $header[0] . ': ' . $header[1] . $EOL; + } + + return $res; + } +} diff --git a/lib/Zend/Registry.php b/lib/Zend/Registry.php new file mode 100644 index 0000000..2bc8814 --- /dev/null +++ b/lib/Zend/Registry.php @@ -0,0 +1,207 @@ +offsetExists($index)) { + require_once 'Zend/Exception.php'; + throw new Zend_Exception("No entry is registered for key '$index'"); + } + + return $instance->offsetGet($index); + } + + /** + * setter method, basically same as offsetSet(). + * + * This method can be called from an object of type Zend_Registry, or it + * can be called statically. In the latter case, it uses the default + * static instance stored in the class. + * + * @param string $index The location in the ArrayObject in which to store + * the value. + * @param mixed $value The object to store in the ArrayObject. + * @return void + */ + public static function set($index, $value) + { + $instance = self::getInstance(); + $instance->offsetSet($index, $value); + } + + /** + * Returns TRUE if the $index is a named value in the registry, + * or FALSE if $index was not found in the registry. + * + * @param string $index + * @return boolean + */ + public static function isRegistered($index) + { + if (self::$_registry === null) { + return false; + } + return self::$_registry->offsetExists($index); + } + + /** + * Constructs a parent ArrayObject with default + * ARRAY_AS_PROPS to allow acces as an object + * + * @param array $array data array + * @param integer $flags ArrayObject flags + */ + public function __construct($array = array(), $flags = parent::ARRAY_AS_PROPS) + { + parent::__construct($array, $flags); + } + + /** + * @param string $index + * @returns mixed + * + * Workaround for http://bugs.php.net/bug.php?id=40442 (ZF-960). + */ + public function offsetExists($index) + { + return array_key_exists($index, $this); + } + +} diff --git a/lib/Zend/Validate.php b/lib/Zend/Validate.php new file mode 100644 index 0000000..b57930a --- /dev/null +++ b/lib/Zend/Validate.php @@ -0,0 +1,171 @@ +_validators[] = array( + 'instance' => $validator, + 'breakChainOnFailure' => (boolean) $breakChainOnFailure + ); + return $this; + } + + /** + * Returns true if and only if $value passes all validations in the chain + * + * Validators are run in the order in which they were added to the chain (FIFO). + * + * @param mixed $value + * @return boolean + */ + public function isValid($value) + { + $this->_messages = array(); + $this->_errors = array(); + $result = true; + foreach ($this->_validators as $element) { + $validator = $element['instance']; + if ($validator->isValid($value)) { + continue; + } + $result = false; + $messages = $validator->getMessages(); + $this->_messages = array_merge($this->_messages, $messages); + $this->_errors = array_merge($this->_errors, array_keys($messages)); + if ($element['breakChainOnFailure']) { + break; + } + } + return $result; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns array of validation failure messages + * + * @return array + */ + public function getMessages() + { + return $this->_messages; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns array of validation failure message codes + * + * @return array + * @deprecated Since 1.5.0 + */ + public function getErrors() + { + return $this->_errors; + } + + /** + * @param mixed $value + * @param string $classBaseName + * @param array $args OPTIONAL + * @param mixed $namespaces OPTIONAL + * @return boolean + * @throws Zend_Validate_Exception + */ + public static function is($value, $classBaseName, array $args = array(), $namespaces = array()) + { + $namespaces = array_merge(array('Zend_Validate'), (array) $namespaces); + foreach ($namespaces as $namespace) { + $className = $namespace . '_' . ucfirst($classBaseName); + try { + require_once 'Zend/Loader.php'; + @Zend_Loader::loadClass($className); + if (class_exists($className, false)) { + $class = new ReflectionClass($className); + if ($class->implementsInterface('Zend_Validate_Interface')) { + if ($class->hasMethod('__construct')) { + $object = $class->newInstanceArgs($args); + } else { + $object = $class->newInstance(); + } + return $object->isValid($value); + } + } + } catch (Zend_Validate_Exception $ze) { + // if there is an exception while validating throw it + throw $ze; + } catch (Zend_Exception $ze) { + // fallthrough and continue for missing validation classes + } + } + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("Validate class not found from basename '$classBaseName'"); + } + +} diff --git a/lib/Zend/Validate/Abstract.php b/lib/Zend/Validate/Abstract.php new file mode 100644 index 0000000..66566a9 --- /dev/null +++ b/lib/Zend/Validate/Abstract.php @@ -0,0 +1,358 @@ +_messages; + } + + /** + * Returns an array of the names of variables that are used in constructing validation failure messages + * + * @return array + */ + public function getMessageVariables() + { + return array_keys($this->_messageVariables); + } + + /** + * Sets the validation failure message template for a particular key + * + * @param string $messageString + * @param string $messageKey OPTIONAL + * @return Zend_Validate_Abstract Provides a fluent interface + * @throws Zend_Validate_Exception + */ + public function setMessage($messageString, $messageKey = null) + { + if ($messageKey === null) { + $keys = array_keys($this->_messageTemplates); + $messageKey = current($keys); + } + if (!isset($this->_messageTemplates[$messageKey])) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("No message template exists for key '$messageKey'"); + } + $this->_messageTemplates[$messageKey] = $messageString; + return $this; + } + + /** + * Sets validation failure message templates given as an array, where the array keys are the message keys, + * and the array values are the message template strings. + * + * @param array $messages + * @return Zend_Validate_Abstract + */ + public function setMessages(array $messages) + { + foreach ($messages as $key => $message) { + $this->setMessage($message, $key); + } + return $this; + } + + /** + * Magic function returns the value of the requested property, if and only if it is the value or a + * message variable. + * + * @param string $property + * @return mixed + * @throws Zend_Validate_Exception + */ + public function __get($property) + { + if ($property == 'value') { + return $this->_value; + } + if (array_key_exists($property, $this->_messageVariables)) { + return $this->{$this->_messageVariables[$property]}; + } + /** + * @see Zend_Validate_Exception + */ + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("No property exists by the name '$property'"); + } + + /** + * Constructs and returns a validation failure message with the given message key and value. + * + * Returns null if and only if $messageKey does not correspond to an existing template. + * + * If a translator is available and a translation exists for $messageKey, + * the translation will be used. + * + * @param string $messageKey + * @param string $value + * @return string + */ + protected function _createMessage($messageKey, $value) + { + if (!isset($this->_messageTemplates[$messageKey])) { + return null; + } + + $message = $this->_messageTemplates[$messageKey]; + + if (null !== ($translator = $this->getTranslator())) { + if ($translator->isTranslated($message)) { + $message = $translator->translate($message); + } elseif ($translator->isTranslated($messageKey)) { + $message = $translator->translate($messageKey); + } + } + + if (is_object($value)) { + if (!in_array('__toString', get_class_methods($value))) { + $value = get_class($value) . ' object'; + } else { + $value = $value->__toString(); + } + } else { + $value = (string)$value; + } + + if ($this->getObscureValue()) { + $value = str_repeat('*', strlen($value)); + } + + $message = str_replace('%value%', (string) $value, $message); + foreach ($this->_messageVariables as $ident => $property) { + $message = str_replace("%$ident%", (string) $this->$property, $message); + } + return $message; + } + + /** + * @param string $messageKey OPTIONAL + * @param string $value OPTIONAL + * @return void + */ + protected function _error($messageKey = null, $value = null) + { + if ($messageKey === null) { + $keys = array_keys($this->_messageTemplates); + $messageKey = current($keys); + } + if ($value === null) { + $value = $this->_value; + } + $this->_errors[] = $messageKey; + $this->_messages[$messageKey] = $this->_createMessage($messageKey, $value); + } + + /** + * Sets the value to be validated and clears the messages and errors arrays + * + * @param mixed $value + * @return void + */ + protected function _setValue($value) + { + $this->_value = $value; + $this->_messages = array(); + $this->_errors = array(); + } + + /** + * Returns array of validation failure message codes + * + * @return array + * @deprecated Since 1.5.0 + */ + public function getErrors() + { + return $this->_errors; + } + + /** + * Set flag indicating whether or not value should be obfuscated in messages + * + * @param bool $flag + * @return Zend_Validate_Abstract + */ + public function setObscureValue($flag) + { + $this->_obscureValue = (bool) $flag; + return $this; + } + + /** + * Retrieve flag indicating whether or not value should be obfuscated in + * messages + * + * @return bool + */ + public function getObscureValue() + { + return $this->_obscureValue; + } + + /** + * Set translation object + * + * @param Zend_Translate|Zend_Translate_Adapter|null $translator + * @return Zend_Validate_Abstract + */ + public function setTranslator($translator = null) + { + if ((null === $translator) || ($translator instanceof Zend_Translate_Adapter)) { + $this->_translator = $translator; + } elseif ($translator instanceof Zend_Translate) { + $this->_translator = $translator->getAdapter(); + } else { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Invalid translator specified'); + } + return $this; + } + + /** + * Return translation object + * + * @return Zend_Translate_Adapter|null + */ + public function getTranslator() + { + if (null === $this->_translator) { + return self::getDefaultTranslator(); + } + + return $this->_translator; + } + + /** + * Set default translation object for all validate objects + * + * @param Zend_Translate|Zend_Translate_Adapter|null $translator + * @return void + */ + public static function setDefaultTranslator($translator = null) + { + if ((null === $translator) || ($translator instanceof Zend_Translate_Adapter)) { + self::$_defaultTranslator = $translator; + } elseif ($translator instanceof Zend_Translate) { + self::$_defaultTranslator = $translator->getAdapter(); + } else { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Invalid translator specified'); + } + } + + /** + * Get default translation object for all validate objects + * + * @return Zend_Translate_Adapter|null + */ + public static function getDefaultTranslator() + { + if (null === self::$_defaultTranslator) { + require_once 'Zend/Registry.php'; + if (Zend_Registry::isRegistered('Zend_Translate')) { + $translator = Zend_Registry::get('Zend_Translate'); + if ($translator instanceof Zend_Translate_Adapter) { + return $translator; + } elseif ($translator instanceof Zend_Translate) { + return $translator->getAdapter(); + } + } + } + return self::$_defaultTranslator; + } +} diff --git a/lib/Zend/Validate/Alnum.php b/lib/Zend/Validate/Alnum.php new file mode 100644 index 0000000..36b0adc --- /dev/null +++ b/lib/Zend/Validate/Alnum.php @@ -0,0 +1,120 @@ + "'%value%' has not only alphabetic and digit characters", + self::STRING_EMPTY => "'%value%' is an empty string" + ); + + /** + * Sets default option values for this instance + * + * @param boolean $allowWhiteSpace + * @return void + */ + public function __construct($allowWhiteSpace = false) + { + $this->allowWhiteSpace = (boolean) $allowWhiteSpace; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value contains only alphabetic and digit characters + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + + $this->_setValue($valueString); + + if ('' === $valueString) { + $this->_error(self::STRING_EMPTY); + return false; + } + + if (null === self::$_filter) { + /** + * @see Zend_Filter_Alnum + */ + require_once 'Zend/Filter/Alnum.php'; + self::$_filter = new Zend_Filter_Alnum(); + } + + self::$_filter->allowWhiteSpace = $this->allowWhiteSpace; + + if ($valueString !== self::$_filter->filter($valueString)) { + $this->_error(self::NOT_ALNUM); + return false; + } + + return true; + } + +} diff --git a/lib/Zend/Validate/Alpha.php b/lib/Zend/Validate/Alpha.php new file mode 100644 index 0000000..0f2298e --- /dev/null +++ b/lib/Zend/Validate/Alpha.php @@ -0,0 +1,120 @@ + "'%value%' has not only alphabetic characters", + self::STRING_EMPTY => "'%value%' is an empty string" + ); + + /** + * Sets default option values for this instance + * + * @param boolean $allowWhiteSpace + * @return void + */ + public function __construct($allowWhiteSpace = false) + { + $this->allowWhiteSpace = (boolean) $allowWhiteSpace; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value contains only alphabetic characters + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + + $this->_setValue($valueString); + + if ('' === $valueString) { + $this->_error(self::STRING_EMPTY); + return false; + } + + if (null === self::$_filter) { + /** + * @see Zend_Filter_Alpha + */ + require_once 'Zend/Filter/Alpha.php'; + self::$_filter = new Zend_Filter_Alpha(); + } + + self::$_filter->allowWhiteSpace = $this->allowWhiteSpace; + + if ($valueString !== self::$_filter->filter($valueString)) { + $this->_error(self::NOT_ALPHA); + return false; + } + + return true; + } + +} diff --git a/lib/Zend/Validate/Barcode.php b/lib/Zend/Validate/Barcode.php new file mode 100644 index 0000000..d51f11b --- /dev/null +++ b/lib/Zend/Validate/Barcode.php @@ -0,0 +1,99 @@ +setType($barcodeType); + } + + /** + * Sets a new barcode validator + * + * @param string $barcodeType - Barcode validator to use + * @return void + * @throws Zend_Validate_Exception + */ + public function setType($barcodeType) + { + switch (strtolower($barcodeType)) { + case 'upc': + case 'upc-a': + $className = 'UpcA'; + break; + case 'ean13': + case 'ean-13': + $className = 'Ean13'; + break; + default: + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("Barcode type '$barcodeType' is not supported'"); + break; + } + + require_once 'Zend/Validate/Barcode/' . $className . '.php'; + + $class = 'Zend_Validate_Barcode_' . $className; + $this->_barcodeValidator = new $class; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value contains a valid barcode + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + return call_user_func(array($this->_barcodeValidator, 'isValid'), $value); + } +} diff --git a/lib/Zend/Validate/Barcode/Ean13.php b/lib/Zend/Validate/Barcode/Ean13.php new file mode 100644 index 0000000..393091d --- /dev/null +++ b/lib/Zend/Validate/Barcode/Ean13.php @@ -0,0 +1,112 @@ + "'%value%' is an invalid EAN-13 barcode", + self::INVALID_LENGTH => "'%value%' should be 13 characters", + self::NOT_NUMERIC => "'%value%' should contain only numeric characters", + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value contains a valid barcode + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + if (false === ctype_digit($value)) { + $this->_error(self::NOT_NUMERIC); + return false; + } + + $valueString = (string) $value; + $this->_setValue($valueString); + + if (strlen($valueString) !== 13) { + $this->_error(self::INVALID_LENGTH); + return false; + } + + $barcode = strrev(substr($valueString, 0, -1)); + $oddSum = 0; + $evenSum = 0; + + for ($i = 0; $i < 12; $i++) { + if ($i % 2 === 0) { + $oddSum += $barcode[$i] * 3; + } elseif ($i % 2 === 1) { + $evenSum += $barcode[$i]; + } + } + + $calculation = ($oddSum + $evenSum) % 10; + $checksum = ($calculation === 0) ? 0 : 10 - $calculation; + + if ($valueString[12] != $checksum) { + $this->_error(self::INVALID); + return false; + } + + return true; + } +} diff --git a/lib/Zend/Validate/Barcode/UpcA.php b/lib/Zend/Validate/Barcode/UpcA.php new file mode 100644 index 0000000..c584e81 --- /dev/null +++ b/lib/Zend/Validate/Barcode/UpcA.php @@ -0,0 +1,100 @@ + "'%value%' is an invalid UPC-A barcode", + self::INVALID_LENGTH => "'%value%' should be 12 characters", + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value contains a valid barcode + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + $this->_setValue($valueString); + + if (strlen($valueString) !== 12) { + $this->_error(self::INVALID_LENGTH); + return false; + } + + $barcode = substr($valueString, 0, -1); + $oddSum = 0; + $evenSum = 0; + + for ($i = 0; $i < 11; $i++) { + if ($i % 2 === 0) { + $oddSum += $barcode[$i] * 3; + } elseif ($i % 2 === 1) { + $evenSum += $barcode[$i]; + } + } + + $calculation = ($oddSum + $evenSum) % 10; + $checksum = ($calculation === 0) ? 0 : 10 - $calculation; + + if ($valueString[11] != $checksum) { + $this->_error(self::INVALID); + return false; + } + + return true; + } +} diff --git a/lib/Zend/Validate/Between.php b/lib/Zend/Validate/Between.php new file mode 100644 index 0000000..bb0b726 --- /dev/null +++ b/lib/Zend/Validate/Between.php @@ -0,0 +1,200 @@ + "'%value%' is not between '%min%' and '%max%', inclusively", + self::NOT_BETWEEN_STRICT => "'%value%' is not strictly between '%min%' and '%max%'" + ); + + /** + * Additional variables available for validation failure messages + * + * @var array + */ + protected $_messageVariables = array( + 'min' => '_min', + 'max' => '_max' + ); + + /** + * Minimum value + * + * @var mixed + */ + protected $_min; + + /** + * Maximum value + * + * @var mixed + */ + protected $_max; + + /** + * Whether to do inclusive comparisons, allowing equivalence to min and/or max + * + * If false, then strict comparisons are done, and the value may equal neither + * the min nor max options + * + * @var boolean + */ + protected $_inclusive; + + /** + * Sets validator options + * + * @param mixed $min + * @param mixed $max + * @param boolean $inclusive + * @return void + */ + public function __construct($min, $max, $inclusive = true) + { + $this->setMin($min) + ->setMax($max) + ->setInclusive($inclusive); + } + + /** + * Returns the min option + * + * @return mixed + */ + public function getMin() + { + return $this->_min; + } + + /** + * Sets the min option + * + * @param mixed $min + * @return Zend_Validate_Between Provides a fluent interface + */ + public function setMin($min) + { + $this->_min = $min; + return $this; + } + + /** + * Returns the max option + * + * @return mixed + */ + public function getMax() + { + return $this->_max; + } + + /** + * Sets the max option + * + * @param mixed $max + * @return Zend_Validate_Between Provides a fluent interface + */ + public function setMax($max) + { + $this->_max = $max; + return $this; + } + + /** + * Returns the inclusive option + * + * @return boolean + */ + public function getInclusive() + { + return $this->_inclusive; + } + + /** + * Sets the inclusive option + * + * @param boolean $inclusive + * @return Zend_Validate_Between Provides a fluent interface + */ + public function setInclusive($inclusive) + { + $this->_inclusive = $inclusive; + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value is between min and max options, inclusively + * if inclusive option is true. + * + * @param mixed $value + * @return boolean + */ + public function isValid($value) + { + $this->_setValue($value); + + if ($this->_inclusive) { + if ($this->_min > $value || $value > $this->_max) { + $this->_error(self::NOT_BETWEEN); + return false; + } + } else { + if ($this->_min >= $value || $value >= $this->_max) { + $this->_error(self::NOT_BETWEEN_STRICT); + return false; + } + } + return true; + } + +} diff --git a/lib/Zend/Validate/Ccnum.php b/lib/Zend/Validate/Ccnum.php new file mode 100644 index 0000000..227a4ec --- /dev/null +++ b/lib/Zend/Validate/Ccnum.php @@ -0,0 +1,111 @@ + "'%value%' must contain between 13 and 19 digits", + self::CHECKSUM => "Luhn algorithm (mod-10 checksum) failed on '%value%'" + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value follows the Luhn algorithm (mod-10 checksum) + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $this->_setValue($value); + + if (null === self::$_filter) { + /** + * @see Zend_Filter_Digits + */ + require_once 'Zend/Filter/Digits.php'; + self::$_filter = new Zend_Filter_Digits(); + } + + $valueFiltered = self::$_filter->filter($value); + + $length = strlen($valueFiltered); + + if ($length < 13 || $length > 19) { + $this->_error(self::LENGTH); + return false; + } + + $sum = 0; + $weight = 2; + + for ($i = $length - 2; $i >= 0; $i--) { + $digit = $weight * $valueFiltered[$i]; + $sum += floor($digit / 10) + $digit % 10; + $weight = $weight % 2 + 1; + } + + if ((10 - $sum % 10) % 10 != $valueFiltered[$length - 1]) { + $this->_error(self::CHECKSUM, $valueFiltered); + return false; + } + + return true; + } + +} diff --git a/lib/Zend/Validate/Date.php b/lib/Zend/Validate/Date.php new file mode 100644 index 0000000..156c584 --- /dev/null +++ b/lib/Zend/Validate/Date.php @@ -0,0 +1,235 @@ + "'%value%' is not of the format YYYY-MM-DD", + self::INVALID => "'%value%' does not appear to be a valid date", + self::FALSEFORMAT => "'%value%' does not fit given date format" + ); + + /** + * Optional format + * + * @var string|null + */ + protected $_format; + + /** + * Optional locale + * + * @var string|Zend_Locale|null + */ + protected $_locale; + + /** + * Sets validator options + * + * @param string $format OPTIONAL + * @param string|Zend_Locale $locale OPTIONAL + * @return void + */ + public function __construct($format = null, $locale = null) + { + $this->setFormat($format); + if ($locale !== null) { + $this->setLocale($locale); + } + } + + /** + * Returns the locale option + * + * @return string|Zend_Locale|null + */ + public function getLocale() + { + return $this->_locale; + } + + /** + * Sets the locale option + * + * @param string|Zend_Locale $locale + * @return Zend_Validate_Date provides a fluent interface + */ + public function setLocale($locale = null) + { + require_once 'Zend/Locale.php'; + $this->_locale = Zend_Locale::findLocale($locale); + return $this; + } + + /** + * Returns the locale option + * + * @return string|null + */ + public function getFormat() + { + return $this->_format; + } + + /** + * Sets the format option + * + * @param string $format + * @return Zend_Validate_Date provides a fluent interface + */ + public function setFormat($format = null) + { + $this->_format = $format; + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if $value is a valid date of the format YYYY-MM-DD + * If optional $format or $locale is set the date format is checked + * according to Zend_Date, see Zend_Date::isDate() + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + + $this->_setValue($valueString); + + if (($this->_format !== null) or ($this->_locale !== null)) { + require_once 'Zend/Date.php'; + if (!Zend_Date::isDate($value, $this->_format, $this->_locale)) { + if ($this->_checkFormat($value) === false) { + $this->_error(self::FALSEFORMAT); + } else { + $this->_error(self::INVALID); + } + return false; + } + } else { + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $valueString)) { + $this->_error(self::NOT_YYYY_MM_DD); + return false; + } + + list($year, $month, $day) = sscanf($valueString, '%d-%d-%d'); + + if (!checkdate($month, $day, $year)) { + $this->_error(self::INVALID); + return false; + } + } + + return true; + } + + /** + * Check if the given date fits the given format + * + * @param string $value Date to check + * @return boolean False when date does not fit the format + */ + private function _checkFormat($value) + { + try { + require_once 'Zend/Locale/Format.php'; + $parsed = Zend_Locale_Format::getDate($value, array( + 'date_format' => $this->_format, 'format_type' => 'iso', + 'fix_date' => false)); + if (isset($parsed['year']) and ((strpos(strtoupper($this->_format), 'YY') !== false) and + (strpos(strtoupper($this->_format), 'YYYY') === false))) { + $parsed['year'] = Zend_Date::getFullYear($parsed['year']); + } + } catch (Exception $e) { + // Date can not be parsed + return false; + } + + if (((strpos($this->_format, 'Y') !== false) or (strpos($this->_format, 'y') !== false)) and + (!isset($parsed['year']))) { + // Year expected but not found + return false; + } + + if ((strpos($this->_format, 'M') !== false) and (!isset($parsed['month']))) { + // Month expected but not found + return false; + } + + if ((strpos($this->_format, 'd') !== false) and (!isset($parsed['day']))) { + // Day expected but not found + return false; + } + + if (((strpos($this->_format, 'H') !== false) or (strpos($this->_format, 'h') !== false)) and + (!isset($parsed['hour']))) { + // Hour expected but not found + return false; + } + + if ((strpos($this->_format, 'm') !== false) and (!isset($parsed['minute']))) { + // Minute expected but not found + return false; + } + + if ((strpos($this->_format, 's') !== false) and (!isset($parsed['second']))) { + // Second expected but not found + return false; + } + + // Date fits the format + return true; + } +} diff --git a/lib/Zend/Validate/Digits.php b/lib/Zend/Validate/Digits.php new file mode 100644 index 0000000..c42ec0a --- /dev/null +++ b/lib/Zend/Validate/Digits.php @@ -0,0 +1,100 @@ + "'%value%' contains not only digit characters", + self::STRING_EMPTY => "'%value%' is an empty string" + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value only contains digit characters + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + + $this->_setValue($valueString); + + if ('' === $valueString) { + $this->_error(self::STRING_EMPTY); + return false; + } + + if (null === self::$_filter) { + /** + * @see Zend_Filter_Digits + */ + require_once 'Zend/Filter/Digits.php'; + self::$_filter = new Zend_Filter_Digits(); + } + + if ($valueString !== self::$_filter->filter($valueString)) { + $this->_error(self::NOT_DIGITS); + return false; + } + + return true; + } + +} diff --git a/lib/Zend/Validate/EmailAddress.php b/lib/Zend/Validate/EmailAddress.php new file mode 100644 index 0000000..82ed91b --- /dev/null +++ b/lib/Zend/Validate/EmailAddress.php @@ -0,0 +1,247 @@ + "'%value%' is not a valid email address in the basic format local-part@hostname", + self::INVALID_HOSTNAME => "'%hostname%' is not a valid hostname for email address '%value%'", + self::INVALID_MX_RECORD => "'%hostname%' does not appear to have a valid MX record for the email address '%value%'", + self::DOT_ATOM => "'%localPart%' not matched against dot-atom format", + self::QUOTED_STRING => "'%localPart%' not matched against quoted-string format", + self::INVALID_LOCAL_PART => "'%localPart%' is not a valid local part for email address '%value%'", + self::LENGTH_EXCEEDED => "'%value%' exceeds the allowed length" + ); + + /** + * @var array + */ + protected $_messageVariables = array( + 'hostname' => '_hostname', + 'localPart' => '_localPart' + ); + + /** + * Local object for validating the hostname part of an email address + * + * @var Zend_Validate_Hostname + */ + public $hostnameValidator; + + /** + * Whether we check for a valid MX record via DNS + * + * @var boolean + */ + protected $_validateMx = false; + + /** + * @var string + */ + protected $_hostname; + + /** + * @var string + */ + protected $_localPart; + + /** + * Instantiates hostname validator for local use + * + * You can pass a bitfield to determine what types of hostnames are allowed. + * These bitfields are defined by the ALLOW_* constants in Zend_Validate_Hostname + * The default is to allow DNS hostnames only + * + * @param integer $allow OPTIONAL + * @param bool $validateMx OPTIONAL + * @param Zend_Validate_Hostname $hostnameValidator OPTIONAL + * @return void + */ + public function __construct($allow = Zend_Validate_Hostname::ALLOW_DNS, $validateMx = false, Zend_Validate_Hostname $hostnameValidator = null) + { + $this->setValidateMx($validateMx); + $this->setHostnameValidator($hostnameValidator, $allow); + } + + /** + * @param Zend_Validate_Hostname $hostnameValidator OPTIONAL + * @param int $allow OPTIONAL + * @return void + */ + public function setHostnameValidator(Zend_Validate_Hostname $hostnameValidator = null, $allow = Zend_Validate_Hostname::ALLOW_DNS) + { + if ($hostnameValidator === null) { + $hostnameValidator = new Zend_Validate_Hostname($allow); + } + $this->hostnameValidator = $hostnameValidator; + } + + /** + * Whether MX checking via dns_get_mx is supported or not + * + * This currently only works on UNIX systems + * + * @return boolean + */ + public function validateMxSupported() + { + return function_exists('dns_get_mx'); + } + + /** + * Set whether we check for a valid MX record via DNS + * + * This only applies when DNS hostnames are validated + * + * @param boolean $allowed Set allowed to true to validate for MX records, and false to not validate them + */ + public function setValidateMx($allowed) + { + $this->_validateMx = (bool) $allowed; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value is a valid email address + * according to RFC2822 + * + * @link http://www.ietf.org/rfc/rfc2822.txt RFC2822 + * @link http://www.columbia.edu/kermit/ascii.html US-ASCII characters + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + $matches = array(); + $length = true; + + $this->_setValue($valueString); + + // Split email address up and disallow '..' + if ((strpos($valueString, '..') !== false) or + (!preg_match('/^(.+)@([^@]+)$/', $valueString, $matches))) { + $this->_error(self::INVALID); + return false; + } + + $this->_localPart = $matches[1]; + $this->_hostname = $matches[2]; + + if ((strlen($this->_localPart) > 64) || (strlen($this->_hostname) > 255)) { + $length = false; + $this->_error(self::LENGTH_EXCEEDED); + } + + // Match hostname part + $hostnameResult = $this->hostnameValidator->setTranslator($this->getTranslator()) + ->isValid($this->_hostname); + if (!$hostnameResult) { + $this->_error(self::INVALID_HOSTNAME); + + // Get messages and errors from hostnameValidator + foreach ($this->hostnameValidator->getMessages() as $code => $message) { + $this->_messages[$code] = $message; + } + foreach ($this->hostnameValidator->getErrors() as $error) { + $this->_errors[] = $error; + } + } else if ($this->_validateMx) { + // MX check on hostname via dns_get_record() + if ($this->validateMxSupported()) { + $result = dns_get_mx($this->_hostname, $mxHosts); + if (count($mxHosts) < 1) { + $hostnameResult = false; + $this->_error(self::INVALID_MX_RECORD); + } + } else { + /** + * MX checks are not supported by this system + * @see Zend_Validate_Exception + */ + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Internal error: MX checking not available on this system'); + } + } + + // First try to match the local part on the common dot-atom format + $localResult = false; + + // Dot-atom characters are: 1*atext *("." 1*atext) + // atext: ALPHA / DIGIT / and "!", "#", "$", "%", "&", "'", "*", + // "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~" + $atext = 'a-zA-Z0-9\x21\x23\x24\x25\x26\x27\x2a\x2b\x2d\x2f\x3d\x3f\x5e\x5f\x60\x7b\x7c\x7d'; + if (preg_match('/^[' . $atext . ']+(\x2e+[' . $atext . ']+)*$/', $this->_localPart)) { + $localResult = true; + } else { + // Try quoted string format + + // Quoted-string characters are: DQUOTE *([FWS] qtext/quoted-pair) [FWS] DQUOTE + // qtext: Non white space controls, and the rest of the US-ASCII characters not + // including "\" or the quote character + $noWsCtl = '\x01-\x08\x0b\x0c\x0e-\x1f\x7f'; + $qtext = $noWsCtl . '\x21\x23-\x5b\x5d-\x7e'; + $ws = '\x20\x09'; + if (preg_match('/^\x22([' . $ws . $qtext . '])*[$ws]?\x22$/', $this->_localPart)) { + $localResult = true; + } else { + $this->_error(self::DOT_ATOM); + $this->_error(self::QUOTED_STRING); + $this->_error(self::INVALID_LOCAL_PART); + } + } + + // If both parts valid, return true + if ($localResult && $hostnameResult && $length) { + return true; + } else { + return false; + } + } +} diff --git a/lib/Zend/Validate/Exception.php b/lib/Zend/Validate/Exception.php new file mode 100644 index 0000000..a38077e --- /dev/null +++ b/lib/Zend/Validate/Exception.php @@ -0,0 +1,37 @@ + "Too much files, maximum '%max%' are allowed but '%count%' are given", + self::TOO_LESS => "Too less files, minimum '%min%' are expected but '%count%' are given" + ); + + /** + * @var array Error message template variables + */ + protected $_messageVariables = array( + 'min' => '_min', + 'max' => '_max', + 'count' => '_count' + ); + + /** + * Minimum file count + * + * If null, there is no minimum file count + * + * @var integer + */ + protected $_min; + + /** + * Maximum file count + * + * If null, there is no maximum file count + * + * @var integer|null + */ + protected $_max; + + /** + * Actual filecount + * + * @var integer + */ + protected $_count; + + /** + * Internal file array + * @var array + */ + protected $_files; + + /** + * Sets validator options + * + * Min limits the file count, when used with max=null it is the maximum file count + * It also accepts an array with the keys 'min' and 'max' + * + * If $options is a integer, it will be used as maximum file count + * As Array is accepts the following keys: + * 'min': Minimum filecount + * 'max': Maximum filecount + * + * @param integer|array $options Options for the adapter + * @param integer $max (Deprecated) Maximum value (implies $options is the minimum) + * @return void + */ + public function __construct($options) + { + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } elseif (is_string($options) || is_numeric($options)) { + $options = array('max' => $options); + } elseif (!is_array($options)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception ('Invalid options to validator provided'); + } + + if (1 < func_num_args()) { + trigger_error('Multiple arguments are deprecated in favor of an array of named arguments', E_USER_NOTICE); + $options['min'] = func_get_arg(0); + $options['max'] = func_get_arg(1); + } + + if (isset($options['min'])) { + $this->setMin($options); + } + + if (isset($options['max'])) { + $this->setMax($options); + } + } + + /** + * Returns the minimum file count + * + * @return integer + */ + public function getMin() + { + return $this->_min; + } + + /** + * Sets the minimum file count + * + * @param integer|array $min The minimum file count + * @return Zend_Validate_File_Count Provides a fluent interface + * @throws Zend_Validate_Exception When min is greater than max + */ + public function setMin($min) + { + if (is_array($min) and isset($min['min'])) { + $min = $min['min']; + } + + if (!is_string($min) and !is_numeric($min)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception ('Invalid options to validator provided'); + } + + $min = (integer) $min; + if (($this->_max !== null) && ($min > $this->_max)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The minimum must be less than or equal to the maximum file count, but $min >" + . " {$this->_max}"); + } + + $this->_min = $min; + return $this; + } + + /** + * Returns the maximum file count + * + * @return integer + */ + public function getMax() + { + return $this->_max; + } + + /** + * Sets the maximum file count + * + * @param integer|array $max The maximum file count + * @return Zend_Validate_StringLength Provides a fluent interface + * @throws Zend_Validate_Exception When max is smaller than min + */ + public function setMax($max) + { + if (is_array($max) and isset($max['max'])) { + $max = $max['max']; + } + + if (!is_string($max) and !is_numeric($max)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception ('Invalid options to validator provided'); + } + + $max = (integer) $max; + if (($this->_min !== null) && ($max < $this->_min)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The maximum must be greater than or equal to the minimum file count, but " + . "$max < {$this->_min}"); + } + + $this->_max = $max; + return $this; + } + + /** + * Adds a file for validation + * + * @param string|array $file + */ + public function addFile($file) + { + if (is_string($file)) { + $file = array($file); + } + + if (is_array($file)) { + foreach ($file as $name) { + if (!isset($this->_files[$name])) { + $this->_files[$name] = $name; + } + } + } + + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the file count of all checked files is at least min and + * not bigger than max (when max is not null). Attention: When checking with set min you + * must give all files with the first call, otherwise you will get an false. + * + * @param string|array $value Filenames to check for count + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + $this->addFile($value); + $this->_count = count($this->_files); + if (($this->_max !== null) && ($this->_count > $this->_max)) { + return $this->_throw($file, self::TOO_MUCH); + } + + if (($this->_min !== null) && ($this->_count < $this->_min)) { + return $this->_throw($file, self::TOO_LESS); + } + + return true; + } + + /** + * Throws an error of the given type + * + * @param string $file + * @param string $errorType + * @return false + */ + protected function _throw($file, $errorType) + { + if ($file !== null) { + $this->_value = $file['name']; + } + + $this->_error($errorType); + return false; + } +} diff --git a/lib/Zend/Validate/File/Crc32.php b/lib/Zend/Validate/File/Crc32.php new file mode 100644 index 0000000..47ace9d --- /dev/null +++ b/lib/Zend/Validate/File/Crc32.php @@ -0,0 +1,179 @@ + "The file '%value%' does not match the given crc32 hashes", + self::NOT_DETECTED => "There was no crc32 hash detected for the given file", + self::NOT_FOUND => "The file '%value%' could not be found" + ); + + /** + * Hash of the file + * + * @var string + */ + protected $_hash; + + /** + * Sets validator options + * + * @param string|array $options + * @return void + */ + public function __construct($options) + { + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } elseif (is_scalar($options)) { + $options = array('hash1' => $options); + } elseif (!is_array($options)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Invalid options to validator provided'); + } + + $this->setCrc32($options); + } + + /** + * Returns all set crc32 hashes + * + * @return array + */ + public function getCrc32() + { + return $this->getHash(); + } + + /** + * Sets the crc32 hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function setHash($options) + { + if (!is_array($options)) { + $options = array($options); + } + + $options['algorithm'] = 'crc32'; + parent::setHash($options); + return $this; + } + + /** + * Sets the crc32 hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function setCrc32($options) + { + $this->setHash($options); + return $this; + } + + /** + * Adds the crc32 hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function addHash($options) + { + if (!is_array($options)) { + $options = array($options); + } + + $options['algorithm'] = 'crc32'; + parent::addHash($options); + return $this; + } + + /** + * Adds the crc32 hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function addCrc32($options) + { + $this->addHash($options); + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the given file confirms the set hash + * + * @param string $value Filename to check for hash + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_FOUND); + } + + $hashes = array_unique(array_keys($this->_hash)); + $filehash = hash_file('crc32', $value); + if ($filehash === false) { + return $this->_throw($file, self::NOT_DETECTED); + } + + foreach($hashes as $hash) { + if ($filehash === $hash) { + return true; + } + } + + return $this->_throw($file, self::DOES_NOT_MATCH); + } +} \ No newline at end of file diff --git a/lib/Zend/Validate/File/ExcludeExtension.php b/lib/Zend/Validate/File/ExcludeExtension.php new file mode 100644 index 0000000..2a442ab --- /dev/null +++ b/lib/Zend/Validate/File/ExcludeExtension.php @@ -0,0 +1,94 @@ + "The file '%value%' has a false extension", + self::NOT_FOUND => "The file '%value%' was not found" + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the fileextension of $value is not included in the + * set extension list + * + * @param string $value Real file to check for extension + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_FOUND); + } + + if ($file !== null) { + $info['extension'] = substr($file['name'], strrpos($file['name'], '.') + 1); + } else { + $info = pathinfo($value); + } + + $extensions = $this->getExtension(); + + if ($this->_case and (!in_array($info['extension'], $extensions))) { + return true; + } else if (!$this->_case) { + $found = false; + foreach ($extensions as $extension) { + if (strtolower($extension) == strtolower($info['extension'])) { + $found = true; + } + } + + if (!$found) { + return true; + } + } + + return $this->_throw($file, self::FALSE_EXTENSION); + } +} diff --git a/lib/Zend/Validate/File/ExcludeMimeType.php b/lib/Zend/Validate/File/ExcludeMimeType.php new file mode 100644 index 0000000..d69dc06 --- /dev/null +++ b/lib/Zend/Validate/File/ExcludeMimeType.php @@ -0,0 +1,91 @@ +_throw($file, self::NOT_READABLE); + } + + if ($file !== null) { + if (class_exists('finfo', false) && defined('MAGIC')) { + $mime = new finfo(FILEINFO_MIME); + $this->_type = $mime->file($value); + unset($mime); + } elseif (function_exists('mime_content_type') && ini_get('mime_magic.magicfile')) { + $this->_type = mime_content_type($value); + } else { + $this->_type = $file['type']; + } + } + + if (empty($this->_type)) { + return $this->_throw($file, self::NOT_DETECTED); + } + + $mimetype = $this->getMimeType(true); + if (in_array($this->_type, $mimetype)) { + return $this->_throw($file, self::FALSE_TYPE); + } + + $types = explode('/', $this->_type); + $types = array_merge($types, explode('-', $this->_type)); + foreach($mimetype as $mime) { + if (in_array($mime, $types)) { + return $this->_throw($file, self::FALSE_TYPE); + } + } + + return true; + } +} diff --git a/lib/Zend/Validate/File/Exists.php b/lib/Zend/Validate/File/Exists.php new file mode 100644 index 0000000..cf509aa --- /dev/null +++ b/lib/Zend/Validate/File/Exists.php @@ -0,0 +1,203 @@ + "The file '%value%' does not exist" + ); + + /** + * Internal list of directories + * @var string + */ + protected $_directory = ''; + + /** + * @var array Error message template variables + */ + protected $_messageVariables = array( + 'directory' => '_directory' + ); + + /** + * Sets validator options + * + * @param string|array $directory + * @return void + */ + public function __construct($directory = array()) + { + if ($directory instanceof Zend_Config) { + $directory = $directory->toArray(); + } else if (is_string($directory)) { + $directory = explode(',', $directory); + } else if (!is_array($directory)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception ('Invalid options to validator provided'); + } + + $this->setDirectory($directory); + } + + /** + * Returns the set file directories which are checked + * + * @param boolean $asArray Returns the values as array, when false an concated string is returned + * @return string + */ + public function getDirectory($asArray = false) + { + $asArray = (bool) $asArray; + $directory = (string) $this->_directory; + if ($asArray) { + $directory = explode(',', $directory); + } + + return $directory; + } + + /** + * Sets the file directory which will be checked + * + * @param string|array $directory The directories to validate + * @return Zend_Validate_File_Extension Provides a fluent interface + */ + public function setDirectory($directory) + { + $this->_directory = null; + $this->addDirectory($directory); + return $this; + } + + /** + * Adds the file directory which will be checked + * + * @param string|array $directory The directory to add for validation + * @return Zend_Validate_File_Extension Provides a fluent interface + */ + public function addDirectory($directory) + { + $directories = $this->getDirectory(true); + + if (is_string($directory)) { + $directory = explode(',', $directory); + } else if (!is_array($directory)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception ('Invalid options to validator provided'); + } + + foreach ($directory as $content) { + if (empty($content) || !is_string($content)) { + continue; + } + + $directories[] = trim($content); + } + $directories = array_unique($directories); + + // Sanity check to ensure no empty values + foreach ($directories as $key => $dir) { + if (empty($dir)) { + unset($directories[$key]); + } + } + + $this->_directory = implode(',', $directories); + + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the file already exists in the set directories + * + * @param string $value Real file to check for existance + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + $directories = $this->getDirectory(true); + if (($file !== null) and (!empty($file['destination']))) { + $directories[] = $file['destination']; + } else if (!isset($file['name'])) { + $file['name'] = $value; + } + + $check = false; + foreach ($directories as $directory) { + if (empty($directory)) { + continue; + } + + $check = true; + if (!file_exists($directory . DIRECTORY_SEPARATOR . $file['name'])) { + return $this->_throw($file, self::DOES_NOT_EXIST); + } + } + + if (!$check) { + return $this->_throw($file, self::DOES_NOT_EXIST); + } + + return true; + } + + /** + * Throws an error of the given type + * + * @param string $file + * @param string $errorType + * @return false + */ + protected function _throw($file, $errorType) + { + if ($file !== null) { + $this->_value = $file['name']; + } + + $this->_error($errorType); + return false; + } +} diff --git a/lib/Zend/Validate/File/Extension.php b/lib/Zend/Validate/File/Extension.php new file mode 100644 index 0000000..49411b3 --- /dev/null +++ b/lib/Zend/Validate/File/Extension.php @@ -0,0 +1,234 @@ + "The file '%value%' has a false extension", + self::NOT_FOUND => "The file '%value%' was not found" + ); + + /** + * Internal list of extensions + * @var string + */ + protected $_extension = ''; + + /** + * Validate case sensitive + * + * @var boolean + */ + protected $_case = false; + + /** + * @var array Error message template variables + */ + protected $_messageVariables = array( + 'extension' => '_extension' + ); + + /** + * Sets validator options + * + * @param string|array $extension + * @param boolean $case If true validation is done case sensitive + * @return void + */ + public function __construct($options) + { + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } + + if (1 < func_num_args()) { + trigger_error('Multiple arguments to constructor are deprecated in favor of options array', E_USER_NOTICE); + $case = func_get_arg(1); + $this->setCase($case); + } + + if (is_array($options) and isset($options['case'])) { + $this->setCase($options['case']); + unset($options['case']); + } + + $this->setExtension($options); + } + + /** + * Returns the case option + * + * @return boolean + */ + public function getCase() + { + return $this->_case; + } + + /** + * Sets the case to use + * + * @param boolean $case + * @return Zend_Validate_File_Extension Provides a fluent interface + */ + public function setCase($case) + { + $this->_case = (boolean) $case; + return $this; + } + + /** + * Returns the set file extension + * + * @return array + */ + public function getExtension() + { + $extension = explode(',', $this->_extension); + + return $extension; + } + + /** + * Sets the file extensions + * + * @param string|array $extension The extensions to validate + * @return Zend_Validate_File_Extension Provides a fluent interface + */ + public function setExtension($extension) + { + $this->_extension = null; + $this->addExtension($extension); + return $this; + } + + /** + * Adds the file extensions + * + * @param string|array $extension The extensions to add for validation + * @return Zend_Validate_File_Extension Provides a fluent interface + */ + public function addExtension($extension) + { + $extensions = $this->getExtension(); + if (is_string($extension)) { + $extension = explode(',', $extension); + } + + foreach ($extension as $content) { + if (empty($content) || !is_string($content)) { + continue; + } + + $extensions[] = trim($content); + } + $extensions = array_unique($extensions); + + // Sanity check to ensure no empty values + foreach ($extensions as $key => $ext) { + if (empty($ext)) { + unset($extensions[$key]); + } + } + + $this->_extension = implode(',', $extensions); + + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the fileextension of $value is included in the + * set extension list + * + * @param string $value Real file to check for extension + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_FOUND); + } + + if ($file !== null) { + $info['extension'] = substr($file['name'], strrpos($file['name'], '.') + 1); + } else { + $info = pathinfo($value); + } + + $extensions = $this->getExtension(); + + if ($this->_case && (in_array($info['extension'], $extensions))) { + return true; + } else if (!$this->getCase()) { + foreach ($extensions as $extension) { + if (strtolower($extension) == strtolower($info['extension'])) { + return true; + } + } + } + + return $this->_throw($file, self::FALSE_EXTENSION); + } + + /** + * Throws an error of the given type + * + * @param string $file + * @param string $errorType + * @return false + */ + protected function _throw($file, $errorType) + { + if (null !== $file) { + $this->_value = $file['name']; + } + + $this->_error($errorType); + return false; + } +} diff --git a/lib/Zend/Validate/File/FilesSize.php b/lib/Zend/Validate/File/FilesSize.php new file mode 100644 index 0000000..ea8d085 --- /dev/null +++ b/lib/Zend/Validate/File/FilesSize.php @@ -0,0 +1,163 @@ + "All files in sum should have a maximum size of '%max%' but '%size%' were detected", + self::TOO_SMALL => "All files in sum should have a minimum size of '%min%' but '%size%' were detected", + self::NOT_READABLE => "One or more files can not be read" + ); + + /** + * Internal file array + * + * @var array + */ + protected $_files; + + /** + * Sets validator options + * + * Min limits the used diskspace for all files, when used with max=null it is the maximum filesize + * It also accepts an array with the keys 'min' and 'max' + * + * @param integer|array $min Minimum diskspace for all files + * @param integer $max Maximum diskspace for all files (deprecated) + * @param boolean $bytestring Use bytestring or real size ? (deprecated) + * @return void + */ + public function __construct($options) + { + $this->_files = array(); + $this->_setSize(0); + + if (1 < func_num_args()) { + trigger_error('Multiple constructor options are deprecated in favor of a single options array', E_USER_NOTICE); + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } elseif (is_scalar($options)) { + $options = array('min' => $options); + } elseif (!is_array($options)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Invalid options to validator provided'); + } + + $argv = func_get_args(); + array_shift($argv); + $options['max'] = array_shift($argv); + if (!empty($argv)) { + $options['bytestring'] = array_shift($argv); + } + } + + parent::__construct($options); + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the disk usage of all files is at least min and + * not bigger than max (when max is not null). + * + * @param string|array $value Real file to check for size + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + require_once 'Zend/Loader.php'; + if (is_string($value)) { + $value = array($value); + } + + $min = $this->getMin(true); + $max = $this->getMax(true); + $size = $this->_getSize(); + foreach ($value as $files) { + // Is file readable ? + if (!Zend_Loader::isReadable($files)) { + $this->_throw($file, self::NOT_READABLE); + continue; + } + + if (!isset($this->_files[$files])) { + $this->_files[$files] = $files; + } else { + // file already counted... do not count twice + continue; + } + + // limited to 2GB files + $size += @filesize($files); + $this->_setSize($size); + if (($max !== null) && ($max < $size)) { + if ($this->useByteString()) { + $this->setMax($this->_toByteString($max)); + $this->_throw($file, self::TOO_BIG); + $this->setMax($max); + } else { + $this->_throw($file, self::TOO_BIG); + } + } + } + + // Check that aggregate files are >= minimum size + if (($min !== null) && ($size < $min)) { + if ($this->useByteString()) { + $this->setMin($this->_toByteString($min)); + $this->_throw($file, self::TOO_SMALL); + $this->setMin($min); + } else { + $this->_throw($file, self::TOO_SMALL); + } + } + + if (count($this->_messages) > 0) { + return false; + } + + return true; + } +} diff --git a/lib/Zend/Validate/File/Hash.php b/lib/Zend/Validate/File/Hash.php new file mode 100644 index 0000000..1702593 --- /dev/null +++ b/lib/Zend/Validate/File/Hash.php @@ -0,0 +1,195 @@ + "The file '%value%' does not match the given hashes", + self::NOT_DETECTED => "There was no hash detected for the given file", + self::NOT_FOUND => "The file '%value%' could not be found" + ); + + /** + * Hash of the file + * + * @var string + */ + protected $_hash; + + /** + * Sets validator options + * + * @param string|array $options + * @return void + */ + public function __construct($options) + { + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } elseif (is_scalar($options)) { + $options = array('hash1' => $options); + } elseif (!is_array($options)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Invalid options to validator provided'); + } + + if (1 < func_num_args()) { + trigger_error('Multiple constructor options are deprecated in favor of a single options array', E_USER_NOTICE); + $options['algorithm'] = func_get_arg(1); + } + + $this->setHash($options); + } + + /** + * Returns the set hash values as array, the hash as key and the algorithm the value + * + * @return array + */ + public function getHash() + { + return $this->_hash; + } + + /** + * Sets the hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function setHash($options) + { + $this->_hash = null; + $this->addHash($options); + + return $this; + } + + /** + * Adds the hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function addHash($options) + { + if (is_string($options)) { + $options = array($options); + } else if (!is_array($options)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("False parameter given"); + } + + $known = hash_algos(); + if (!isset($options['algorithm'])) { + $algorithm = 'crc32'; + } else { + $algorithm = $options['algorithm']; + unset($options['algorithm']); + } + + if (!in_array($algorithm, $known)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("Unknown algorithm '{$algorithm}'"); + } + + foreach ($options as $value) { + $this->_hash[$value] = $algorithm; + } + + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the given file confirms the set hash + * + * @param string $value Filename to check for hash + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_FOUND); + } + + $algos = array_unique(array_values($this->_hash)); + $hashes = array_unique(array_keys($this->_hash)); + foreach ($algos as $algorithm) { + $filehash = hash_file($algorithm, $value); + if ($filehash === false) { + return $this->_throw($file, self::NOT_DETECTED); + } + + foreach($hashes as $hash) { + if ($filehash === $hash) { + return true; + } + } + } + + return $this->_throw($file, self::DOES_NOT_MATCH); + } + + /** + * Throws an error of the given type + * + * @param string $file + * @param string $errorType + * @return false + */ + protected function _throw($file, $errorType) + { + if ($file !== null) { + $this->_value = $file['name']; + } + + $this->_error($errorType); + return false; + } +} diff --git a/lib/Zend/Validate/File/ImageSize.php b/lib/Zend/Validate/File/ImageSize.php new file mode 100644 index 0000000..ada8bd3 --- /dev/null +++ b/lib/Zend/Validate/File/ImageSize.php @@ -0,0 +1,370 @@ + "Maximum allowed width for image '%value%' should be '%maxwidth%' but '%width%' detected", + self::WIDTH_TOO_SMALL => "Minimum expected width for image '%value%' should be '%minwidth%' but '%width%' detected", + self::HEIGHT_TOO_BIG => "Maximum allowed height for image '%value%' should be '%maxheight%' but '%height%' detected", + self::HEIGHT_TOO_SMALL => "Minimum expected height for image '%value%' should be '%minheight%' but '%height%' detected", + self::NOT_DETECTED => "The size of image '%value%' could not be detected", + self::NOT_READABLE => "The image '%value%' can not be read" + ); + + /** + * @var array Error message template variables + */ + protected $_messageVariables = array( + 'minwidth' => '_minwidth', + 'maxwidth' => '_maxwidth', + 'minheight' => '_minheight', + 'maxheight' => '_maxheight', + 'width' => '_width', + 'height' => '_height' + ); + + /** + * Minimum image width + * + * @var integer + */ + protected $_minwidth; + + /** + * Maximum image width + * + * @var integer + */ + protected $_maxwidth; + + /** + * Minimum image height + * + * @var integer + */ + protected $_minheight; + + /** + * Maximum image height + * + * @var integer + */ + protected $_maxheight; + + /** + * Detected width + * + * @var integer + */ + protected $_width; + + /** + * Detected height + * + * @var integer + */ + protected $_height; + + /** + * Sets validator options + * + * Accepts the following option keys: + * - minheight + * - minwidth + * - maxheight + * - maxwidth + * + * @param Zend_Config|array $options + * @return void + */ + public function __construct($options) + { + $minwidth = 0; + $minheight = 0; + $maxwidth = null; + $maxheight = null; + + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } elseif (1 < func_num_args()) { + trigger_error('Multiple constructor options are deprecated in favor of a single options array', E_USER_NOTICE); + if (!is_array($options)) { + $options = array('minwidth' => $options); + } + $argv = func_get_args(); + array_shift($argv); + $options['minheight'] = array_shift($argv); + if (!empty($argv)) { + $options['maxwidth'] = array_shift($argv); + if (!empty($argv)) { + $options['maxheight'] = array_shift($argv); + } + } + } else if (!is_array($options)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception ('Invalid options to validator provided'); + } + + if (isset($options['minheight']) || isset($options['minwidth'])) { + $this->setImageMin($options); + } + + if (isset($options['maxheight']) || isset($options['maxwidth'])) { + $this->setImageMax($options); + } + } + + /** + * Returns the set minimum image sizes + * + * @return array + */ + public function getImageMin() + { + return array('minwidth' => $this->_minwidth, 'minheight' => $this->_minheight); + } + + /** + * Returns the set maximum image sizes + * + * @return array + */ + public function getImageMax() + { + return array('maxwidth' => $this->_maxwidth, 'maxheight' => $this->_maxheight); + } + + /** + * Returns the set image width sizes + * + * @return array + */ + public function getImageWidth() + { + return array('minwidth' => $this->_minwidth, 'maxwidth' => $this->_maxwidth); + } + + /** + * Returns the set image height sizes + * + * @return array + */ + public function getImageHeight() + { + return array('minheight' => $this->_minheight, 'maxheight' => $this->_maxheight); + } + + /** + * Sets the minimum image size + * + * @param array $options The minimum image dimensions + * @throws Zend_Validate_Exception When minwidth is greater than maxwidth + * @throws Zend_Validate_Exception When minheight is greater than maxheight + * @return Zend_Validate_File_ImageSize Provides a fluent interface + */ + public function setImageMin($options) + { + if (isset($options['minwidth'])) { + if (($this->_maxwidth !== null) and ($options['minwidth'] > $this->_maxwidth)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The minimum image width must be less than or equal to the " + . " maximum image width, but {$options['minwidth']} > {$this->_maxwidth}"); + } + } + + if (isset($options['maxheight'])) { + if (($this->_maxheight !== null) and ($options['minheight'] > $this->_maxheight)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The minimum image height must be less than or equal to the " + . " maximum image height, but {$options['minheight']} > {$this->_maxheight}"); + } + } + + if (isset($options['minwidth'])) { + $this->_minwidth = (int) $options['minwidth']; + } + + if (isset($options['minheight'])) { + $this->_minheight = (int) $options['minheight']; + } + + return $this; + } + + /** + * Sets the maximum image size + * + * @param array $options The maximum image dimensions + * @throws Zend_Validate_Exception When maxwidth is smaller than minwidth + * @throws Zend_Validate_Exception When maxheight is smaller than minheight + * @return Zend_Validate_StringLength Provides a fluent interface + */ + public function setImageMax($options) + { + if (isset($options['maxwidth'])) { + if (($this->_minwidth !== null) and ($options['maxwidth'] < $this->_minwidth)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The maximum image width must be greater than or equal to the " + . "minimum image width, but {$options['maxwidth']} < {$this->_minwidth}"); + } + } + + if (isset($options['maxheight'])) { + if (($this->_minheight !== null) and ($options['maxheight'] < $this->_minheight)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The maximum image height must be greater than or equal to the " + . "minimum image height, but {$options['maxheight']} < {$this->_minwidth}"); + } + } + + if (isset($options['maxwidth'])) { + $this->_maxwidth = (int) $options['maxwidth']; + } + + if (isset($options['maxheight'])) { + $this->_maxheight = (int) $options['maxheight']; + } + + return $this; + } + + /** + * Sets the mimimum and maximum image width + * + * @param array $options The image width dimensions + * @return Zend_Validate_File_ImageSize Provides a fluent interface + */ + public function setImageWidth($options) + { + $this->setImageMin($options); + $this->setImageMax($options); + + return $this; + } + + /** + * Sets the mimimum and maximum image height + * + * @param array $options The image height dimensions + * @return Zend_Validate_File_ImageSize Provides a fluent interface + */ + public function setImageHeight($options) + { + $this->setImageMin($options); + $this->setImageMax($options); + + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the imagesize of $value is at least min and + * not bigger than max + * + * @param string $value Real file to check for image size + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_READABLE); + } + + $size = @getimagesize($value); + $this->_setValue($file); + + if (empty($size) or ($size[0] === 0) or ($size[1] === 0)) { + return $this->_throw($file, self::NOT_DETECTED); + } + + $this->_width = $size[0]; + $this->_height = $size[1]; + if ($this->_width < $this->_minwidth) { + $this->_throw($file, self::WIDTH_TOO_SMALL); + } + + if (($this->_maxwidth !== null) and ($this->_maxwidth < $this->_width)) { + $this->_throw($file, self::WIDTH_TOO_BIG); + } + + if ($this->_height < $this->_minheight) { + $this->_throw($file, self::HEIGHT_TOO_SMALL); + } + + if (($this->_maxheight !== null) and ($this->_maxheight < $this->_height)) { + $this->_throw($file, self::HEIGHT_TOO_BIG); + } + + if (count($this->_messages) > 0) { + return false; + } + + return true; + } + + /** + * Throws an error of the given type + * + * @param string $file + * @param string $errorType + * @return false + */ + protected function _throw($file, $errorType) + { + if ($file !== null) { + $this->_value = $file['name']; + } + + $this->_error($errorType); + return false; + } +} diff --git a/lib/Zend/Validate/File/IsCompressed.php b/lib/Zend/Validate/File/IsCompressed.php new file mode 100644 index 0000000..050eb1c --- /dev/null +++ b/lib/Zend/Validate/File/IsCompressed.php @@ -0,0 +1,133 @@ + "The file '%value%' is not compressed, '%type%' detected", + self::NOT_DETECTED => "The mimetype of file '%value%' has not been detected", + self::NOT_READABLE => "The file '%value%' can not be read" + ); + + /** + * Sets validator options + * + * @param string|array $compression + * @return void + */ + public function __construct($mimetype = array()) + { + if (empty($mimetype)) { + $mimetype = array( + 'application/x-tar', + 'application/x-cpio', + 'application/x-debian-package', + 'application/x-archive', + 'application/x-arc', + 'application/x-arj', + 'application/x-lharc', + 'application/x-lha', + 'application/x-rar', + 'application/zip', + 'application/zoo', + 'application/x-eet', + 'application/x-java-pack200', + 'application/x-compress', + 'application/x-gzip', + 'application/x-bzip2' + ); + } + + $this->setMimeType($mimetype); + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the file is compression with the set compression types + * + * @param string $value Real file to check for compression + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_READABLE); + } + + if ($file !== null) { + if (class_exists('finfo', false) && defined('MAGIC')) { + $mime = new finfo(FILEINFO_MIME); + $this->_type = $mime->file($value); + unset($mime); + } elseif (function_exists('mime_content_type') && ini_get('mime_magic.magicfile')) { + $this->_type = mime_content_type($value); + } else { + $this->_type = $file['type']; + } + } + + if (empty($this->_type)) { + return $this->_throw($file, self::NOT_DETECTED); + } + + $compressions = $this->getMimeType(true); + if (in_array($this->_type, $compressions)) { + return true; + } + + $types = explode('/', $this->_type); + $types = array_merge($types, explode('-', $this->_type)); + foreach ($compressions as $mime) { + if (in_array($mime, $types)) { + return true; + } + } + + return $this->_throw($file, self::FALSE_TYPE); + } +} diff --git a/lib/Zend/Validate/File/IsImage.php b/lib/Zend/Validate/File/IsImage.php new file mode 100644 index 0000000..045c7fe --- /dev/null +++ b/lib/Zend/Validate/File/IsImage.php @@ -0,0 +1,137 @@ + "The file '%value%' is no image, '%type%' detected", + self::NOT_DETECTED => "The mimetype of file '%value%' has not been detected", + self::NOT_READABLE => "The file '%value%' can not be read" + ); + + /** + * Sets validator options + * + * @param string|array $mimetype + * @return void + */ + public function __construct($mimetype = array()) + { + if (empty($mimetype)) { + $mimetype = array( + 'image/x-quicktime', + 'image/jp2', + 'image/x-xpmi', + 'image/x-portable-bitmap', + 'image/x-portable-greymap', + 'image/x-portable-pixmap', + 'image/x-niff', + 'image/tiff', + 'image/png', + 'image/x-unknown', + 'image/gif', + 'image/x-ms-bmp', + 'application/dicom', + 'image/vnd.adobe.photoshop', + 'image/vnd.djvu', + 'image/x-cpi', + 'image/jpeg', + 'image/x-ico', + 'image/x-coreldraw', + 'image/svg+xml' + ); + } + + $this->setMimeType($mimetype); + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the file is compression with the set compression types + * + * @param string $value Real file to check for compression + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_READABLE); + } + + if ($file !== null) { + if (class_exists('finfo', false) && defined('MAGIC')) { + $mime = new finfo(FILEINFO_MIME); + $this->_type = $mime->file($value); + unset($mime); + } elseif (function_exists('mime_content_type') && ini_get('mime_magic.magicfile')) { + $this->_type = mime_content_type($value); + } else { + $this->_type = $file['type']; + } + } + + if (empty($this->_type)) { + return $this->_throw($file, self::NOT_DETECTED); + } + + $compressions = $this->getMimeType(true); + if (in_array($this->_type, $compressions)) { + return true; + } + + $types = explode('/', $this->_type); + $types = array_merge($types, explode('-', $this->_type)); + foreach($compressions as $mime) { + if (in_array($mime, $types)) { + return true; + } + } + + return $this->_throw($file, self::FALSE_TYPE); + } +} diff --git a/lib/Zend/Validate/File/Md5.php b/lib/Zend/Validate/File/Md5.php new file mode 100644 index 0000000..4b612d0 --- /dev/null +++ b/lib/Zend/Validate/File/Md5.php @@ -0,0 +1,183 @@ + "The file '%value%' does not match the given md5 hashes", + self::NOT_DETECTED => "There was no md5 hash detected for the given file", + self::NOT_FOUND => "The file '%value%' could not be found" + ); + + /** + * Hash of the file + * + * @var string + */ + protected $_hash; + + /** + * Sets validator options + * + * $hash is the hash we accept for the file $file + * + * @param string|array $options + * @return void + */ + public function __construct($options) + { + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } elseif (is_scalar($options)) { + $options = array('hash1' => $options); + } elseif (!is_array($options)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Invalid options to validator provided'); + } + + $this->setMd5($options); + } + + /** + * Returns all set md5 hashes + * + * @return array + */ + public function getMd5() + { + return $this->getHash(); + } + + /** + * Sets the md5 hash for one or multiple files + * + * @param string|array $options + * @param string $algorithm (Deprecated) Algorithm to use, fixed to md5 + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function setHash($options) + { + if (!is_array($options)) { + $options = (array) $options; + } + + $options['algorithm'] = 'md5'; + parent::setHash($options); + return $this; + } + + /** + * Sets the md5 hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function setMd5($options) + { + $this->setHash($options); + return $this; + } + + /** + * Adds the md5 hash for one or multiple files + * + * @param string|array $options + * @param string $algorithm (Depreciated) Algorithm to use, fixed to md5 + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function addHash($options) + { + if (!is_array($options)) { + $options = (array) $options; + } + + $options['algorithm'] = 'md5'; + parent::addHash($options); + return $this; + } + + /** + * Adds the md5 hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function addMd5($options) + { + $this->addHash($options); + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the given file confirms the set hash + * + * @param string $value Filename to check for hash + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_FOUND); + } + + $hashes = array_unique(array_keys($this->_hash)); + $filehash = hash_file('md5', $value); + if ($filehash === false) { + return $this->_throw($file, self::NOT_DETECTED); + } + + foreach($hashes as $hash) { + if ($filehash === $hash) { + return true; + } + } + + return $this->_throw($file, self::DOES_NOT_MATCH); + } +} diff --git a/lib/Zend/Validate/File/MimeType.php b/lib/Zend/Validate/File/MimeType.php new file mode 100644 index 0000000..1605dad --- /dev/null +++ b/lib/Zend/Validate/File/MimeType.php @@ -0,0 +1,283 @@ + "The file '%value%' has a false mimetype of '%type%'", + self::NOT_DETECTED => "The mimetype of file '%value%' could not been detected", + self::NOT_READABLE => "The file '%value%' can not be read" + ); + + /** + * @var array + */ + protected $_messageVariables = array( + 'type' => '_type' + ); + + /** + * @var string + */ + protected $_type; + + /** + * Mimetypes + * + * If null, there is no mimetype + * + * @var string|null + */ + protected $_mimetype; + + /** + * Magicfile to use + * + * @var string|null + */ + protected $_magicfile; + + /** + * Sets validator options + * + * Mimetype to accept + * + * @param string|array $mimetype MimeType + * @return void + */ + public function __construct($mimetype) + { + if ($mimetype instanceof Zend_Config) { + $mimetype = $mimetype->toArray(); + } elseif (is_string($mimetype)) { + $mimetype = explode(',', $mimetype); + } elseif (!is_array($mimetype)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("Invalid options to validator provided"); + } + + if (isset($mimetype['magicfile'])) { + $this->setMagicFile($mimetype['magicfile']); + } + + $this->setMimeType($mimetype); + } + + /** + * Returna the actual set magicfile + * + * @return string + */ + public function getMagicFile() + { + return $this->_magicfile; + } + + /** + * Sets the magicfile to use + * if null, the MAGIC constant from php is used + * + * @param string $file + * @return Zend_Validate_File_MimeType Provides fluid interface + */ + public function setMagicFile($file) + { + if (empty($file)) { + $this->_magicfile = null; + } else if (!is_readable($file)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('The given magicfile can not be read'); + } else { + $this->_magicfile = (string) $file; + } + + return $this; + } + + /** + * Returns the set mimetypes + * + * @param boolean $asArray Returns the values as array, when false an concated string is returned + * @return string|array + */ + public function getMimeType($asArray = false) + { + $asArray = (bool) $asArray; + $mimetype = (string) $this->_mimetype; + if ($asArray) { + $mimetype = explode(',', $mimetype); + } + + return $mimetype; + } + + /** + * Sets the mimetypes + * + * @param string|array $mimetype The mimetypes to validate + * @return Zend_Validate_File_Extension Provides a fluent interface + */ + public function setMimeType($mimetype) + { + $this->_mimetype = null; + $this->addMimeType($mimetype); + return $this; + } + + /** + * Adds the mimetypes + * + * @param string|array $mimetype The mimetypes to add for validation + * @return Zend_Validate_File_Extension Provides a fluent interface + */ + public function addMimeType($mimetype) + { + $mimetypes = $this->getMimeType(true); + + if (is_string($mimetype)) { + $mimetype = explode(',', $mimetype); + } elseif (!is_array($mimetype)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("Invalid options to validator provided"); + } + + if (isset($mimetype['magicfile'])) { + unset($mimetype['magicfile']); + } + + foreach ($mimetype as $content) { + if (empty($content) || !is_string($content)) { + continue; + } + $mimetypes[] = trim($content); + } + $mimetypes = array_unique($mimetypes); + + // Sanity check to ensure no empty values + foreach ($mimetypes as $key => $mt) { + if (empty($mt)) { + unset($mimetypes[$key]); + } + } + + $this->_mimetype = implode(',', $mimetypes); + + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if the mimetype of the file matches the given ones. Also parts + * of mimetypes can be checked. If you give for example "image" all image + * mime types will be accepted like "image/gif", "image/jpeg" and so on. + * + * @param string $value Real file to check for mimetype + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_READABLE); + } + + if ($file !== null) { + $mimefile = $this->getMagicFile(); + if (class_exists('finfo', false) && ((!empty($mimefile)) or (defined('MAGIC')))) { + if (!empty($mimefile)) { + $mime = new finfo(FILEINFO_MIME, $mimefile); + } else { + $mime = new finfo(FILEINFO_MIME); + } + + $this->_type = $mime->file($value); + unset($mime); + } elseif (function_exists('mime_content_type') && ini_get('mime_magic.magicfile')) { + $this->_type = mime_content_type($value); + } else { + $this->_type = $file['type']; + } + } + + if (empty($this->_type)) { + return $this->_throw($file, self::NOT_DETECTED); + } + + $mimetype = $this->getMimeType(true); + if (in_array($this->_type, $mimetype)) { + return true; + } + + $types = explode('/', $this->_type); + $types = array_merge($types, explode('-', $this->_type)); + foreach($mimetype as $mime) { + if (in_array($mime, $types)) { + return true; + } + } + + return $this->_throw($file, self::FALSE_TYPE); + } + + /** + * Throws an error of the given type + * + * @param string $file + * @param string $errorType + * @return false + */ + protected function _throw($file, $errorType) + { + if ($file !== null) { + $this->_value = $file['name']; + } + + $this->_error($errorType); + return false; + } +} diff --git a/lib/Zend/Validate/File/NotExists.php b/lib/Zend/Validate/File/NotExists.php new file mode 100644 index 0000000..8aeef9f --- /dev/null +++ b/lib/Zend/Validate/File/NotExists.php @@ -0,0 +1,84 @@ + "The file '%value%' does exist" + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the file does not exist in the set destinations + * + * @param string $value Real file to check for + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + $directories = $this->getDirectory(true); + if (($file !== null) and (!empty($file['destination']))) { + $directories[] = $file['destination']; + } else if (!isset($file['name'])) { + $file['name'] = $value; + } + + foreach ($directories as $directory) { + if (empty($directory)) { + continue; + } + + $check = true; + if (file_exists($directory . DIRECTORY_SEPARATOR . $file['name'])) { + return $this->_throw($file, self::DOES_EXIST); + } + } + + if (!isset($check)) { + return $this->_throw($file, self::DOES_EXIST); + } + + return true; + } +} diff --git a/lib/Zend/Validate/File/Sha1.php b/lib/Zend/Validate/File/Sha1.php new file mode 100644 index 0000000..b02a837 --- /dev/null +++ b/lib/Zend/Validate/File/Sha1.php @@ -0,0 +1,181 @@ + "The file '%value%' does not match the given sha1 hashes", + self::NOT_DETECTED => "There was no sha1 hash detected for the given file", + self::NOT_FOUND => "The file '%value%' could not be found" + ); + + /** + * Hash of the file + * + * @var string + */ + protected $_hash; + + /** + * Sets validator options + * + * $hash is the hash we accept for the file $file + * + * @param string|array $options + * @return void + */ + public function __construct($options) + { + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } elseif (is_scalar($options)) { + $options = array('hash1' => $options); + } elseif (!is_array($options)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Invalid options to validator provided'); + } + + $this->setHash($options); + } + + /** + * Returns all set sha1 hashes + * + * @return array + */ + public function getSha1() + { + return $this->getHash(); + } + + /** + * Sets the sha1 hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function setHash($options) + { + if (!is_array($options)) { + $options = (array) $options; + } + + $options['algorithm'] = 'sha1'; + parent::setHash($options); + return $this; + } + + /** + * Sets the sha1 hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function setSha1($options) + { + $this->setHash($options); + return $this; + } + + /** + * Adds the sha1 hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function addHash($options) + { + if (!is_array($options)) { + $options = (array) $options; + } + + $options['algorithm'] = 'sha1'; + parent::addHash($options); + return $this; + } + + /** + * Adds the sha1 hash for one or multiple files + * + * @param string|array $options + * @return Zend_Validate_File_Hash Provides a fluent interface + */ + public function addSha1($options) + { + $this->addHash($options); + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the given file confirms the set hash + * + * @param string $value Filename to check for hash + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_FOUND); + } + + $hashes = array_unique(array_keys($this->_hash)); + $filehash = hash_file('sha1', $value); + if ($filehash === false) { + return $this->_throw($file, self::NOT_DETECTED); + } + + foreach ($hashes as $hash) { + if ($filehash === $hash) { + return true; + } + } + + return $this->_throw($file, self::DOES_NOT_MATCH); + } +} diff --git a/lib/Zend/Validate/File/Size.php b/lib/Zend/Validate/File/Size.php new file mode 100644 index 0000000..a52d499 --- /dev/null +++ b/lib/Zend/Validate/File/Size.php @@ -0,0 +1,404 @@ + "Maximum allowed size for file '%value%' is '%max%' but '%size%' detected", + self::TOO_SMALL => "Minimum expected size for file '%value%' is '%min%' but '%size%' detected", + self::NOT_FOUND => "The file '%value%' could not be found" + ); + + /** + * @var array Error message template variables + */ + protected $_messageVariables = array( + 'min' => '_min', + 'max' => '_max', + 'size' => '_size', + ); + + /** + * Minimum filesize + * @var integer + */ + protected $_min; + + /** + * Maximum filesize + * + * If null, there is no maximum filesize + * + * @var integer|null + */ + protected $_max; + + /** + * Detected size + * + * @var integer + */ + protected $_size; + + /** + * Use bytestring ? + * + * @var boolean + */ + protected $_useByteString = true; + + /** + * Sets validator options + * + * If $options is a integer, it will be used as maximum filesize + * As Array is accepts the following keys: + * 'min': Minimum filesize + * 'max': Maximum filesize + * 'bytestring': Use bytestring or real size for messages + * + * @param integer|array $options Options for the adapter + */ + public function __construct($options) + { + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } elseif (is_string($options) || is_numeric($options)) { + $options = array('max' => $options); + } elseif (!is_array($options)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception ('Invalid options to validator provided'); + } + + if (1 < func_num_args()) { + trigger_error('Multiple constructor options are deprecated in favor of a single options array', E_USER_NOTICE); + $argv = func_get_args(); + array_shift($argv); + $options['max'] = array_shift($argv); + if (!empty($argv)) { + $options['bytestring'] = array_shift($argv); + } + } + + if (isset($options['bytestring'])) { + $this->setUseByteString($options['bytestring']); + } + + if (isset($options['min'])) { + $this->setMin($options['min']); + } + + if (isset($options['max'])) { + $this->setMax($options['max']); + } + } + + /** + * Returns the minimum filesize + * + * @param boolean $byteString Use bytestring ? + * @return integer + */ + public function setUseByteString($byteString = true) + { + $this->_useByteString = (bool) $byteString; + return $this; + } + + /** + * Will bytestring be used? + * + * @return boolean + */ + public function useByteString() + { + return $this->_useByteString; + } + + /** + * Returns the minimum filesize + * + * @param bool $raw Whether or not to force return of the raw value (defaults off) + * @return integer|string + */ + public function getMin($raw = false) + { + $min = $this->_min; + if (!$raw && $this->useByteString()) { + $min = $this->_toByteString($min); + } + + return $min; + } + + /** + * Sets the minimum filesize + * + * @param integer $min The minimum filesize + * @throws Zend_Validate_Exception When min is greater than max + * @return Zend_Validate_File_Size Provides a fluent interface + */ + public function setMin($min) + { + if (!is_string($min) and !is_numeric($min)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception ('Invalid options to validator provided'); + } + + $min = (integer) $this->_fromByteString($min); + $max = $this->getMax(true); + if (($max !== null) && ($min > $max)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The minimum must be less than or equal to the maximum filesize, but $min >" + . " $max"); + } + + $this->_min = $min; + return $this; + } + + /** + * Returns the maximum filesize + * + * @param bool $raw Whether or not to force return of the raw value (defaults off) + * @return integer|string + */ + public function getMax($raw = false) + { + $max = $this->_max; + if (!$raw && $this->useByteString()) { + $max = $this->_toByteString($max); + } + + return $max; + } + + /** + * Sets the maximum filesize + * + * @param integer $max The maximum filesize + * @throws Zend_Validate_Exception When max is smaller than min + * @return Zend_Validate_StringLength Provides a fluent interface + */ + public function setMax($max) + { + if (!is_string($max) && !is_numeric($max)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception ('Invalid options to validator provided'); + } + + $max = (integer) $this->_fromByteString($max); + $min = $this->getMin(true); + if (($min !== null) && ($max < $min)) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The maximum must be greater than or equal to the minimum filesize, but " + . "$max < $min"); + } + + $this->_max = $max; + return $this; + } + + /** + * Retrieve current detected file size + * + * @return int + */ + protected function _getSize() + { + return $this->_size; + } + + /** + * Set current size + * + * @param int $size + * @return Zend_Validate_File_Size + */ + protected function _setSize($size) + { + $this->_size = $size; + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the filesize of $value is at least min and + * not bigger than max (when max is not null). + * + * @param string $value Real file to check for size + * @param array $file File data from Zend_File_Transfer + * @return boolean + */ + public function isValid($value, $file = null) + { + // Is file readable ? + require_once 'Zend/Loader.php'; + if (!Zend_Loader::isReadable($value)) { + return $this->_throw($file, self::NOT_FOUND); + } + + // limited to 4GB files + $size = sprintf("%u", @filesize($value)); + + // Check to see if it's smaller than min size + $min = $this->getMin(true); + $max = $this->getMax(true); + if (($min !== null) && ($size < $min)) { + if ($this->useByteString()) { + $this->_min = $this->_toByteString($min); + $this->_size = $this->_toByteString($size); + $this->_throw($file, self::TOO_SMALL); + $this->_min = $min; + $this->_size = $size; + } else { + $this->_throw($file, self::TOO_SMALL); + } + } + + // Check to see if it's larger than max size + if (($max !== null) && ($max < $size)) { + if ($this->useByteString()) { + $this->_max = $this->_toByteString($max); + $this->_size = $this->_toByteString($size); + $this->_throw($file, self::TOO_BIG); + $this->_max = $max; + $this->_size = $size; + } else { + $this->_throw($file, self::TOO_BIG); + } + } + + if (count($this->_messages) > 0) { + return false; + } + + return true; + } + + /** + * Returns the formatted size + * + * @param integer $size + * @return string + */ + protected function _toByteString($size) + { + $sizes = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'); + for ($i=0; $size >= 1024 && $i < 9; $i++) { + $size /= 1024; + } + + return round($size, 2) . $sizes[$i]; + } + + /** + * Returns the unformatted size + * + * @param string $size + * @return integer + */ + protected function _fromByteString($size) + { + if (is_numeric($size)) { + return (integer) $size; + } + + $type = trim(substr($size, -2, 1)); + + $value = substr($size, 0, -1); + if (!is_numeric($value)) { + $value = substr($value, 0, -1); + } + + switch (strtoupper($type)) { + case 'Y': + $value *= (1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024); + break; + case 'Z': + $value *= (1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024); + break; + case 'E': + $value *= (1024 * 1024 * 1024 * 1024 * 1024 * 1024); + break; + case 'P': + $value *= (1024 * 1024 * 1024 * 1024 * 1024); + break; + case 'T': + $value *= (1024 * 1024 * 1024 * 1024); + break; + case 'G': + $value *= (1024 * 1024 * 1024); + break; + case 'M': + $value *= (1024 * 1024); + break; + case 'K': + $value *= 1024; + break; + default: + break; + } + + return $value; + } + + /** + * Throws an error of the given type + * + * @param string $file + * @param string $errorType + * @return false + */ + protected function _throw($file, $errorType) + { + if ($file !== null) { + $this->_value = $file['name']; + } + + $this->_error($errorType); + return false; + } +} diff --git a/lib/Zend/Validate/File/Upload.php b/lib/Zend/Validate/File/Upload.php new file mode 100644 index 0000000..ddeb328 --- /dev/null +++ b/lib/Zend/Validate/File/Upload.php @@ -0,0 +1,234 @@ + "The file '%value%' exceeds the defined ini size", + self::FORM_SIZE => "The file '%value%' exceeds the defined form size", + self::PARTIAL => "The file '%value%' was only partially uploaded", + self::NO_FILE => "The file '%value%' was not uploaded", + self::NO_TMP_DIR => "No temporary directory was found for the file '%value%'", + self::CANT_WRITE => "The file '%value%' can't be written", + self::EXTENSION => "The extension returned an error while uploading the file '%value%'", + self::ATTACK => "The file '%value%' was illegal uploaded, possible attack", + self::FILE_NOT_FOUND => "The file '%value%' was not found", + self::UNKNOWN => "Unknown error while uploading the file '%value%'" + ); + + /** + * Internal array of files + * @var array + */ + protected $_files = array(); + + /** + * Sets validator options + * + * The array $files must be given in syntax of Zend_File_Transfer to be checked + * If no files are given the $_FILES array will be used automatically. + * NOTE: This validator will only work with HTTP POST uploads! + * + * @param array $files Array of files in syntax of Zend_File_Transfer + * @return void + */ + public function __construct($files = array()) + { + $this->setFiles($files); + } + + /** + * Returns the array of set files + * + * @param string $files (Optional) The file to return in detail + * @return array + * @throws Zend_Validate_Exception If file is not found + */ + public function getFiles($file = null) + { + if ($file !== null) { + $return = array(); + foreach ($this->_files as $name => $content) { + if ($name === $file) { + $return[$file] = $this->_files[$name]; + } + + if ($content['name'] === $file) { + $return[$name] = $this->_files[$name]; + } + } + + if (count($return) === 0) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The file '$file' was not found"); + } + + return $return; + } + + return $this->_files; + } + + /** + * Sets the minimum filesize + * + * @param array $files The files to check in syntax of Zend_File_Transfer + * @return Zend_Validate_File_Upload Provides a fluent interface + */ + public function setFiles($files = array()) + { + if (count($files) === 0) { + $this->_files = $_FILES; + } else { + $this->_files = $files; + } + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the file was uploaded without errors + * + * @param string $value Single file to check for upload errors, when giving null the $_FILES array + * from initialization will be used + * @return boolean + */ + public function isValid($value, $file = null) + { + if (array_key_exists($value, $this->_files)) { + $files[$value] = $this->_files[$value]; + } else { + foreach ($this->_files as $file => $content) { + if ($content['name'] === $value) { + $files[$file] = $this->_files[$file]; + } + + if ($content['tmp_name'] === $value) { + $files[$file] = $this->_files[$file]; + } + } + } + + if (empty($files)) { + return $this->_throw($file, self::FILE_NOT_FOUND); + } + + foreach ($files as $file => $content) { + $this->_value = $file; + switch($content['error']) { + case 0: + if (!is_uploaded_file($content['tmp_name'])) { + $this->_throw($file, self::ATTACK); + } + break; + + case 1: + $this->_throw($file, self::INI_SIZE); + break; + + case 2: + $this->_throw($file, self::FORM_SIZE); + break; + + case 3: + $this->_throw($file, self::PARTIAL); + break; + + case 4: + $this->_throw($file, self::NO_FILE); + break; + + case 6: + $this->_throw($file, self::NO_TMP_DIR); + break; + + case 7: + $this->_throw($file, self::CANT_WRITE); + break; + + case 8: + $this->_throw($file, self::EXTENSION); + break; + + default: + $this->_throw($file, self::UNKNOWN); + break; + } + } + + if (count($this->_messages) > 0) { + return false; + } else { + return true; + } + } + + /** + * Throws an error of the given type + * + * @param string $file + * @param string $errorType + * @return false + */ + protected function _throw($file, $errorType) + { + if ($file !== null) { + if (is_array($file) and !empty($file['name'])) { + $this->_value = $file['name']; + } + } + + $this->_error($errorType); + return false; + } +} diff --git a/lib/Zend/Validate/Float.php b/lib/Zend/Validate/Float.php new file mode 100644 index 0000000..0405161 --- /dev/null +++ b/lib/Zend/Validate/Float.php @@ -0,0 +1,75 @@ + "'%value%' does not appear to be a float" + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value is a floating-point value + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + + $this->_setValue($valueString); + + $locale = localeconv(); + + $valueFiltered = str_replace($locale['thousands_sep'], '', $valueString); + $valueFiltered = str_replace($locale['decimal_point'], '.', $valueFiltered); + + if (strval(floatval($valueFiltered)) != $valueFiltered) { + $this->_error(); + return false; + } + + return true; + } + +} diff --git a/lib/Zend/Validate/GreaterThan.php b/lib/Zend/Validate/GreaterThan.php new file mode 100644 index 0000000..35e658c --- /dev/null +++ b/lib/Zend/Validate/GreaterThan.php @@ -0,0 +1,114 @@ + "'%value%' is not greater than '%min%'" + ); + + /** + * @var array + */ + protected $_messageVariables = array( + 'min' => '_min' + ); + + /** + * Minimum value + * + * @var mixed + */ + protected $_min; + + /** + * Sets validator options + * + * @param mixed $min + * @return void + */ + public function __construct($min) + { + $this->setMin($min); + } + + /** + * Returns the min option + * + * @return mixed + */ + public function getMin() + { + return $this->_min; + } + + /** + * Sets the min option + * + * @param mixed $min + * @return Zend_Validate_GreaterThan Provides a fluent interface + */ + public function setMin($min) + { + $this->_min = $min; + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value is greater than min option + * + * @param mixed $value + * @return boolean + */ + public function isValid($value) + { + $this->_setValue($value); + + if ($this->_min >= $value) { + $this->_error(); + return false; + } + return true; + } + +} diff --git a/lib/Zend/Validate/Hex.php b/lib/Zend/Validate/Hex.php new file mode 100644 index 0000000..9512eda --- /dev/null +++ b/lib/Zend/Validate/Hex.php @@ -0,0 +1,74 @@ + "'%value%' has not only hexadecimal digit characters" + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value contains only hexadecimal digit characters + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + + $this->_setValue($valueString); + + if (!ctype_xdigit($valueString)) { + $this->_error(); + return false; + } + + return true; + } + +} diff --git a/lib/Zend/Validate/Hostname.php b/lib/Zend/Validate/Hostname.php new file mode 100644 index 0000000..61b0b23 --- /dev/null +++ b/lib/Zend/Validate/Hostname.php @@ -0,0 +1,444 @@ + "'%value%' appears to be an IP address, but IP addresses are not allowed", + self::UNKNOWN_TLD => "'%value%' appears to be a DNS hostname but cannot match TLD against known list", + self::INVALID_DASH => "'%value%' appears to be a DNS hostname but contains a dash (-) in an invalid position", + self::INVALID_HOSTNAME_SCHEMA => "'%value%' appears to be a DNS hostname but cannot match against hostname schema for TLD '%tld%'", + self::UNDECIPHERABLE_TLD => "'%value%' appears to be a DNS hostname but cannot extract TLD part", + self::INVALID_HOSTNAME => "'%value%' does not match the expected structure for a DNS hostname", + self::INVALID_LOCAL_NAME => "'%value%' does not appear to be a valid local network name", + self::LOCAL_NAME_NOT_ALLOWED => "'%value%' appears to be a local network name but local network names are not allowed" + ); + + /** + * @var array + */ + protected $_messageVariables = array( + 'tld' => '_tld' + ); + + /** + * Allows Internet domain names (e.g., example.com) + */ + const ALLOW_DNS = 1; + + /** + * Allows IP addresses + */ + const ALLOW_IP = 2; + + /** + * Allows local network names (e.g., localhost, www.localdomain) + */ + const ALLOW_LOCAL = 4; + + /** + * Allows all types of hostnames + */ + const ALLOW_ALL = 7; + + /** + * Whether IDN domains are validated + * + * @var boolean + */ + private $_validateIdn = true; + + /** + * Whether TLDs are validated against a known list + * + * @var boolean + */ + private $_validateTld = true; + + /** + * Bit field of ALLOW constants; determines which types of hostnames are allowed + * + * @var integer + */ + protected $_allow; + + /** + * Bit field of CHECK constants; determines what additional hostname checks to make + * + * @var unknown_type + */ + // protected $_check; + + /** + * Array of valid top-level-domains + * + * @var array + * @see ftp://data.iana.org/TLD/tlds-alpha-by-domain.txt List of all TLDs by domain + */ + protected $_validTlds = array( + 'ac', 'ad', 'ae', 'aero', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', + 'aq', 'ar', 'arpa', 'as', 'asia', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', + 'bd', 'be', 'bf', 'bg', 'bh', 'bi', 'biz', 'bj', 'bm', 'bn', 'bo', + 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cat', 'cc', 'cd', + 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'com', 'coop', + 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', + 'dz', 'ec', 'edu', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', + 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', + 'gi', 'gl', 'gm', 'gn', 'gov', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', + 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', + 'im', 'in', 'info', 'int', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', + 'jo', 'jobs', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', + 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', + 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mil', 'mk', 'ml', 'mm', + 'mn', 'mo', 'mobi', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'museum', 'mv', + 'mw', 'mx', 'my', 'mz', 'na', 'name', 'nc', 'ne', 'net', 'nf', 'ng', + 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'org', 'pa', 'pe', + 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'pro', 'ps', 'pt', + 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd', + 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', + 'st', 'su', 'sv', 'sy', 'sz', 'tc', 'td', 'tel', 'tf', 'tg', 'th', 'tj', + 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'travel', 'tt', 'tv', 'tw', + 'tz', 'ua', 'ug', 'uk', 'um', 'us', 'uy', 'uz', 'va', 'vc', 've', + 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye', 'yt', 'yu', 'za', 'zm', + 'zw' + ); + + /** + * @var string + */ + protected $_tld; + + /** + * Sets validator options + * + * @param integer $allow OPTIONAL Set what types of hostname to allow (default ALLOW_DNS) + * @param boolean $validateIdn OPTIONAL Set whether IDN domains are validated (default true) + * @param boolean $validateTld OPTIONAL Set whether the TLD element of a hostname is validated (default true) + * @param Zend_Validate_Ip $ipValidator OPTIONAL + * @return void + * @see http://www.iana.org/cctld/specifications-policies-cctlds-01apr02.htm Technical Specifications for ccTLDs + */ + public function __construct($allow = self::ALLOW_DNS, $validateIdn = true, $validateTld = true, Zend_Validate_Ip $ipValidator = null) + { + // Set allow options + $this->setAllow($allow); + + // Set validation options + $this->_validateIdn = $validateIdn; + $this->_validateTld = $validateTld; + + $this->setIpValidator($ipValidator); + } + + /** + * @param Zend_Validate_Ip $ipValidator OPTIONAL + * @return void; + */ + public function setIpValidator(Zend_Validate_Ip $ipValidator = null) + { + if ($ipValidator === null) { + $ipValidator = new Zend_Validate_Ip(); + } + $this->_ipValidator = $ipValidator; + } + + /** + * Returns the allow option + * + * @return integer + */ + public function getAllow() + { + return $this->_allow; + } + + /** + * Sets the allow option + * + * @param integer $allow + * @return Zend_Validate_Hostname Provides a fluent interface + */ + public function setAllow($allow) + { + $this->_allow = $allow; + return $this; + } + + /** + * Set whether IDN domains are validated + * + * This only applies when DNS hostnames are validated + * + * @param boolean $allowed Set allowed to true to validate IDNs, and false to not validate them + */ + public function setValidateIdn ($allowed) + { + $this->_validateIdn = (bool) $allowed; + } + + /** + * Set whether the TLD element of a hostname is validated + * + * This only applies when DNS hostnames are validated + * + * @param boolean $allowed Set allowed to true to validate TLDs, and false to not validate them + */ + public function setValidateTld ($allowed) + { + $this->_validateTld = (bool) $allowed; + } + + /** + * Sets the check option + * + * @param integer $check + * @return Zend_Validate_Hostname Provides a fluent interface + */ + /* + public function setCheck($check) + { + $this->_check = $check; + return $this; + } + */ + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the $value is a valid hostname with respect to the current allow option + * + * @param string $value + * @throws Zend_Validate_Exception if a fatal error occurs for validation process + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + + $this->_setValue($valueString); + + // Check input against IP address schema + if ($this->_ipValidator->setTranslator($this->getTranslator())->isValid($valueString)) { + if (!($this->_allow & self::ALLOW_IP)) { + $this->_error(self::IP_ADDRESS_NOT_ALLOWED); + return false; + } else{ + return true; + } + } + + // Check input against DNS hostname schema + $domainParts = explode('.', $valueString); + if ((count($domainParts) > 1) && (strlen($valueString) >= 4) && (strlen($valueString) <= 254)) { + $status = false; + + do { + // First check TLD + if (preg_match('/([a-z]{2,10})$/i', end($domainParts), $matches)) { + + reset($domainParts); + + // Hostname characters are: *(label dot)(label dot label); max 254 chars + // label: id-prefix [*ldh{61} id-prefix]; max 63 chars + // id-prefix: alpha / digit + // ldh: alpha / digit / dash + + // Match TLD against known list + $this->_tld = strtolower($matches[1]); + if ($this->_validateTld) { + if (!in_array($this->_tld, $this->_validTlds)) { + $this->_error(self::UNKNOWN_TLD); + $status = false; + break; + } + } + + /** + * Match against IDN hostnames + * @see Zend_Validate_Hostname_Interface + */ + $labelChars = 'a-z0-9'; + $utf8 = false; + $classFile = 'Zend/Validate/Hostname/' . ucfirst($this->_tld) . '.php'; + if ($this->_validateIdn) { + if (Zend_Loader::isReadable($classFile)) { + + // Load additional characters + $className = 'Zend_Validate_Hostname_' . ucfirst($this->_tld); + Zend_Loader::loadClass($className); + $labelChars .= call_user_func(array($className, 'getCharacters')); + $utf8 = true; + } + } + + // Keep label regex short to avoid issues with long patterns when matching IDN hostnames + $regexLabel = '/^[' . $labelChars . '\x2d]{1,63}$/i'; + if ($utf8) { + $regexLabel .= 'u'; + } + + // Check each hostname part + $valid = true; + foreach ($domainParts as $domainPart) { + + // Check dash (-) does not start, end or appear in 3rd and 4th positions + if (strpos($domainPart, '-') === 0 || + (strlen($domainPart) > 2 && strpos($domainPart, '-', 2) == 2 && strpos($domainPart, '-', 3) == 3) || + strrpos($domainPart, '-') === strlen($domainPart) - 1) { + + $this->_error(self::INVALID_DASH); + $status = false; + break 2; + } + + // Check each domain part + $status = @preg_match($regexLabel, $domainPart); + if ($status === false) { + /** + * Regex error + * @see Zend_Validate_Exception + */ + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Internal error: DNS validation failed'); + } elseif ($status === 0) { + $valid = false; + } + } + + // If all labels didn't match, the hostname is invalid + if (!$valid) { + $this->_error(self::INVALID_HOSTNAME_SCHEMA); + $status = false; + } + + } else { + // Hostname not long enough + $this->_error(self::UNDECIPHERABLE_TLD); + $status = false; + } + } while (false); + + // If the input passes as an Internet domain name, and domain names are allowed, then the hostname + // passes validation + if ($status && ($this->_allow & self::ALLOW_DNS)) { + return true; + } + } else { + $this->_error(self::INVALID_HOSTNAME); + } + + // Check input against local network name schema; last chance to pass validation + $regexLocal = '/^(([a-zA-Z0-9\x2d]{1,63}\x2e)*[a-zA-Z0-9\x2d]{1,63}){1,254}$/'; + $status = @preg_match($regexLocal, $valueString); + if (false === $status) { + /** + * Regex error + * @see Zend_Validate_Exception + */ + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Internal error: local network name validation failed'); + } + + // If the input passes as a local network name, and local network names are allowed, then the + // hostname passes validation + $allowLocal = $this->_allow & self::ALLOW_LOCAL; + if ($status && $allowLocal) { + return true; + } + + // If the input does not pass as a local network name, add a message + if (!$status) { + $this->_error(self::INVALID_LOCAL_NAME); + } + + // If local network names are not allowed, add a message + if ($status && !$allowLocal) { + $this->_error(self::LOCAL_NAME_NOT_ALLOWED); + } + + return false; + } + + /** + * Throws an exception if a regex for $type does not exist + * + * @param string $type + * @throws Zend_Validate_Exception + * @return Zend_Validate_Hostname Provides a fluent interface + */ + /* + protected function _checkRegexType($type) + { + if (!isset($this->_regex[$type])) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("'$type' must be one of ('" . implode(', ', array_keys($this->_regex)) + . "')"); + } + return $this; + } + */ + +} diff --git a/lib/Zend/Validate/Hostname/At.php b/lib/Zend/Validate/Hostname/At.php new file mode 100644 index 0000000..fff6bf2 --- /dev/null +++ b/lib/Zend/Validate/Hostname/At.php @@ -0,0 +1,50 @@ + 'Tokens do not match', + self::MISSING_TOKEN => 'No token was provided to match against', + ); + + /** + * Original token against which to validate + * @var string + */ + protected $_token; + + /** + * Sets validator options + * + * @param string $token + * @return void + */ + public function __construct($token = null) + { + if (null !== $token) { + $this->setToken($token); + } + } + + /** + * Set token against which to compare + * + * @param string $token + * @return Zend_Validate_Identical + */ + public function setToken($token) + { + $this->_token = (string) $token; + return $this; + } + + /** + * Retrieve token + * + * @return string + */ + public function getToken() + { + return $this->_token; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if a token has been set and the provided value + * matches that token. + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $this->_setValue($value); + $token = $this->getToken(); + + if (empty($token)) { + $this->_error(self::MISSING_TOKEN); + return false; + } + + if ($value !== $token) { + $this->_error(self::NOT_SAME); + return false; + } + + return true; + } +} diff --git a/lib/Zend/Validate/InArray.php b/lib/Zend/Validate/InArray.php new file mode 100644 index 0000000..1c7725a --- /dev/null +++ b/lib/Zend/Validate/InArray.php @@ -0,0 +1,138 @@ + "'%value%' was not found in the haystack" + ); + + /** + * Haystack of possible values + * + * @var array + */ + protected $_haystack; + + /** + * Whether a strict in_array() invocation is used + * + * @var boolean + */ + protected $_strict; + + /** + * Sets validator options + * + * @param array $haystack + * @param boolean $strict + * @return void + */ + public function __construct(array $haystack, $strict = false) + { + $this->setHaystack($haystack) + ->setStrict($strict); + } + + /** + * Returns the haystack option + * + * @return mixed + */ + public function getHaystack() + { + return $this->_haystack; + } + + /** + * Sets the haystack option + * + * @param mixed $haystack + * @return Zend_Validate_InArray Provides a fluent interface + */ + public function setHaystack(array $haystack) + { + $this->_haystack = $haystack; + return $this; + } + + /** + * Returns the strict option + * + * @return boolean + */ + public function getStrict() + { + return $this->_strict; + } + + /** + * Sets the strict option + * + * @param boolean $strict + * @return Zend_Validate_InArray Provides a fluent interface + */ + public function setStrict($strict) + { + $this->_strict = $strict; + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value is contained in the haystack option. If the strict + * option is true, then the type of $value is also checked. + * + * @param mixed $value + * @return boolean + */ + public function isValid($value) + { + $this->_setValue($value); + if (!in_array($value, $this->_haystack, $this->_strict)) { + $this->_error(); + return false; + } + return true; + } + +} diff --git a/lib/Zend/Validate/Int.php b/lib/Zend/Validate/Int.php new file mode 100644 index 0000000..1fa0744 --- /dev/null +++ b/lib/Zend/Validate/Int.php @@ -0,0 +1,73 @@ + "'%value%' does not appear to be an integer" + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value is a valid integer + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + $this->_setValue($valueString); + if (is_bool($value)) { + $this->_error(); + return false; + } + + $locale = localeconv(); + $valueFiltered = str_replace($locale['decimal_point'], '.', $valueString); + $valueFiltered = str_replace($locale['thousands_sep'], '', $valueFiltered); + + if (strval(intval($valueFiltered)) != $valueFiltered) { + $this->_error(); + return false; + } + + return true; + } + +} diff --git a/lib/Zend/Validate/Interface.php b/lib/Zend/Validate/Interface.php new file mode 100644 index 0000000..4fcd525 --- /dev/null +++ b/lib/Zend/Validate/Interface.php @@ -0,0 +1,71 @@ + "'%value%' does not appear to be a valid IP address" + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value is a valid IP address + * + * @param mixed $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + + $this->_setValue($valueString); + + if ((ip2long($valueString) === false) || (long2ip(ip2long($valueString)) !== $valueString)) { + if (!function_exists('inet_pton')) { + $this->_error(); + return false; + } else if ((@inet_pton($value) === false) ||(inet_ntop(@inet_pton($value)) !== $valueString)) { + $this->_error(); + return false; + } + } + + return true; + } + +} diff --git a/lib/Zend/Validate/LessThan.php b/lib/Zend/Validate/LessThan.php new file mode 100644 index 0000000..9f7b72c --- /dev/null +++ b/lib/Zend/Validate/LessThan.php @@ -0,0 +1,113 @@ + "'%value%' is not less than '%max%'" + ); + + /** + * @var array + */ + protected $_messageVariables = array( + 'max' => '_max' + ); + + /** + * Maximum value + * + * @var mixed + */ + protected $_max; + + /** + * Sets validator options + * + * @param mixed $max + * @return void + */ + public function __construct($max) + { + $this->setMax($max); + } + + /** + * Returns the max option + * + * @return mixed + */ + public function getMax() + { + return $this->_max; + } + + /** + * Sets the max option + * + * @param mixed $max + * @return Zend_Validate_LessThan Provides a fluent interface + */ + public function setMax($max) + { + $this->_max = $max; + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value is less than max option + * + * @param mixed $value + * @return boolean + */ + public function isValid($value) + { + $this->_setValue($value); + if ($this->_max <= $value) { + $this->_error(); + return false; + } + return true; + } + +} diff --git a/lib/Zend/Validate/NotEmpty.php b/lib/Zend/Validate/NotEmpty.php new file mode 100644 index 0000000..ce5b1d3 --- /dev/null +++ b/lib/Zend/Validate/NotEmpty.php @@ -0,0 +1,70 @@ + "Value is required and can't be empty" + ); + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value is not an empty value. + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $this->_setValue((string) $value); + + if (is_string($value) + && (('' === $value) + || preg_match('/^\s+$/s', $value)) + ) { + $this->_error(); + return false; + } elseif (!is_string($value) && empty($value)) { + $this->_error(); + return false; + } + + return true; + } + +} diff --git a/lib/Zend/Validate/Regex.php b/lib/Zend/Validate/Regex.php new file mode 100644 index 0000000..1566f07 --- /dev/null +++ b/lib/Zend/Validate/Regex.php @@ -0,0 +1,125 @@ + "'%value%' does not match against pattern '%pattern%'" + ); + + /** + * @var array + */ + protected $_messageVariables = array( + 'pattern' => '_pattern' + ); + + /** + * Regular expression pattern + * + * @var string + */ + protected $_pattern; + + /** + * Sets validator options + * + * @param string $pattern + * @return void + */ + public function __construct($pattern) + { + $this->setPattern($pattern); + } + + /** + * Returns the pattern option + * + * @return string + */ + public function getPattern() + { + return $this->_pattern; + } + + /** + * Sets the pattern option + * + * @param string $pattern + * @return Zend_Validate_Regex Provides a fluent interface + */ + public function setPattern($pattern) + { + $this->_pattern = (string) $pattern; + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if $value matches against the pattern option + * + * @param string $value + * @throws Zend_Validate_Exception if there is a fatal error in pattern matching + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + + $this->_setValue($valueString); + + $status = @preg_match($this->_pattern, $valueString); + if (false === $status) { + /** + * @see Zend_Validate_Exception + */ + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("Internal error matching pattern '$this->_pattern' against value '$valueString'"); + } + if (!$status) { + $this->_error(); + return false; + } + return true; + } + +} diff --git a/lib/Zend/Validate/StringLength.php b/lib/Zend/Validate/StringLength.php new file mode 100644 index 0000000..caf3d4d --- /dev/null +++ b/lib/Zend/Validate/StringLength.php @@ -0,0 +1,223 @@ + "'%value%' is less than %min% characters long", + self::TOO_LONG => "'%value%' is greater than %max% characters long" + ); + + /** + * @var array + */ + protected $_messageVariables = array( + 'min' => '_min', + 'max' => '_max' + ); + + /** + * Minimum length + * + * @var integer + */ + protected $_min; + + /** + * Maximum length + * + * If null, there is no maximum length + * + * @var integer|null + */ + protected $_max; + + /** + * Encoding to use + * + * @var string|null + */ + protected $_encoding; + + /** + * Sets validator options + * + * @param integer $min + * @param integer $max + * @return void + */ + public function __construct($min = 0, $max = null, $encoding = null) + { + $this->setMin($min); + $this->setMax($max); + $this->setEncoding($encoding); + } + + /** + * Returns the min option + * + * @return integer + */ + public function getMin() + { + return $this->_min; + } + + /** + * Sets the min option + * + * @param integer $min + * @throws Zend_Validate_Exception + * @return Zend_Validate_StringLength Provides a fluent interface + */ + public function setMin($min) + { + if (null !== $this->_max && $min > $this->_max) { + /** + * @see Zend_Validate_Exception + */ + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The minimum must be less than or equal to the maximum length, but $min >" + . " $this->_max"); + } + $this->_min = max(0, (integer) $min); + return $this; + } + + /** + * Returns the max option + * + * @return integer|null + */ + public function getMax() + { + return $this->_max; + } + + /** + * Sets the max option + * + * @param integer|null $max + * @throws Zend_Validate_Exception + * @return Zend_Validate_StringLength Provides a fluent interface + */ + public function setMax($max) + { + if (null === $max) { + $this->_max = null; + } else if ($max < $this->_min) { + /** + * @see Zend_Validate_Exception + */ + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception("The maximum must be greater than or equal to the minimum length, but " + . "$max < $this->_min"); + } else { + $this->_max = (integer) $max; + } + + return $this; + } + + /** + * Returns the actual encoding + * + * @return string + */ + public function getEncoding() + { + return $this->_encoding; + } + + /** + * Sets a new encoding to use + * + * @param string $encoding + * @return Zend_Validate_StringLength + */ + public function setEncoding($encoding = null) + { + if ($encoding !== null) { + $orig = iconv_get_encoding('internal_encoding'); + $result = iconv_set_encoding('internal_encoding', $encoding); + if (!$result) { + require_once 'Zend/Validate/Exception.php'; + throw new Zend_Validate_Exception('Given encoding not supported on this OS!'); + } + + iconv_set_encoding('internal_encoding', $orig); + } + + $this->_encoding = $encoding; + return $this; + } + + /** + * Defined by Zend_Validate_Interface + * + * Returns true if and only if the string length of $value is at least the min option and + * no greater than the max option (when the max option is not null). + * + * @param string $value + * @return boolean + */ + public function isValid($value) + { + $valueString = (string) $value; + $this->_setValue($valueString); + if ($this->_encoding !== null) { + $length = iconv_strlen($valueString, $this->_encoding); + } else { + $length = iconv_strlen($valueString); + } + + if ($length < $this->_min) { + $this->_error(self::TOO_SHORT); + } + + if (null !== $this->_max && $this->_max < $length) { + $this->_error(self::TOO_LONG); + } + + if (count($this->_messages)) { + return false; + } else { + return true; + } + } +} diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3