From 65f2d12bbf382d65cb525d3885e4bbd2a02c515c Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 3 May 2010 16:49:59 -0700 Subject: [PATCH 01/16] extlibs updates: PEAR::Mail to 1.2.0, PEAR::Net_SMTP to 1.4.2 (need to go together as a pair) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PEAR::Mail updated to 1.2.0 from 1.1.4, fixes deprecation warnings on PHP 5.3, as well as: 1.2.0: • QA release - stable. • Updated minimum dependencies (Net_SMTP, PEAR, PHP) • Doc Bug #15620 Licence change to BSD • Bug #13659 Mail parse error in special condition • Bug #16200 - Security hole allow to read/write Arbitrary File _hasUnclosedQuotes() doesn't properly handle a double slash before an end quote (slusarz@curecanti.org, Bug #9137). • Make sure Net_SMTP is defined when calling getSMTPObject() directly (slusarz@curecanti.org, Bug #13772). • Add addServiceExtensionParameter() to the SMTP driver (slusarz@curecanti.org, Bug #13764). • Add a method to obtain the Net_SMTP object from the SMTP driver (slusarz@curecanti.org, Bug #13766). PEAR::Net_SMTP updated to 1.4.2 from 1.3.1, needed to support updated PEAR::Mail: 1.4.2: • Fixing header string quoting in data(). (Bug #17199) 1.4.1: • The auth() method now includes an optional $tls parameter that determines whether or not TLS should be attempted (if supported by the PHP runtime and the remote SMTP server). This parameter defaults to true. (Bug #16349) • Header data can be specified separately from message body data by passing it as the optional second parameter to ``data()``. This is especially useful when an open file resource is being used to supply message data because it allows header fields (like *Subject:*) to be built dynamically at runtime. (Request #17012) 1.4.0: • The data() method now accepts either a string or a file resource containing the message data. (Request #16962) 1.3.4: • All Net_Socket write failures are now recognized. (Bug #16831) 1.3.3: • Added getGreeting(), for retrieving the server's greeting string. (Request #16066) [needed for PEAR::Mail] • We no longer attempt a TLS connection if we're already using a secure socket. (Bug #16254) • You can now specify a debug output handler via setDebug(). (Request #16420) 1.3.2: • TLS connection only gets started if no AUTH methods are sent. (Bug #14944) --- extlib/Mail.php | 82 +++++++++++----- extlib/Mail/RFC822.php | 83 +++++++++------- extlib/Mail/mail.php | 63 +++++++++---- extlib/Mail/mock.php | 64 +++++++++---- extlib/Mail/null.php | 64 +++++++++---- extlib/Mail/sendmail.php | 7 +- extlib/Mail/smtp.php | 73 +++++++++++---- extlib/Mail/smtpmx.php | 44 +++++++-- extlib/Net/SMTP.php | 198 ++++++++++++++++++++++++++++++--------- 9 files changed, 481 insertions(+), 197 deletions(-) mode change 100644 => 100755 extlib/Mail.php mode change 100644 => 100755 extlib/Mail/RFC822.php mode change 100644 => 100755 extlib/Mail/mail.php mode change 100644 => 100755 extlib/Mail/mock.php mode change 100644 => 100755 extlib/Mail/null.php mode change 100644 => 100755 extlib/Mail/sendmail.php mode change 100644 => 100755 extlib/Mail/smtp.php mode change 100644 => 100755 extlib/Mail/smtpmx.php diff --git a/extlib/Mail.php b/extlib/Mail.php old mode 100644 new mode 100755 index 3a0c1a9cb8..75132ac2a6 --- a/extlib/Mail.php +++ b/extlib/Mail.php @@ -1,22 +1,47 @@ | -// +----------------------------------------------------------------------+ -// -// $Id: Mail.php,v 1.17 2006/09/15 03:41:18 jon Exp $ +/** + * PEAR's Mail:: interface. + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2002-2007, Richard Heyes + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Chuck Hagenbuch + * @copyright 1997-2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Mail.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ require_once 'PEAR.php'; @@ -26,7 +51,7 @@ require_once 'PEAR.php'; * useful in multiple mailer backends. * * @access public - * @version $Revision: 1.17 $ + * @version $Revision: 294747 $ * @package Mail */ class Mail @@ -82,12 +107,20 @@ class Mail * @return mixed Returns true on success, or a PEAR_Error * containing a descriptive error message on * failure. + * * @access public * @deprecated use Mail_mail::send instead */ function send($recipients, $headers, $body) { - $this->_sanitizeHeaders($headers); + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + + $result = $this->_sanitizeHeaders($headers); + if (is_a($result, 'PEAR_Error')) { + return $result; + } // if we're passed an array of recipients, implode it. if (is_array($recipients)) { @@ -103,10 +136,9 @@ class Mail } // flatten the headers out. - list(,$text_headers) = Mail::prepareHeaders($headers); + list(, $text_headers) = Mail::prepareHeaders($headers); return mail($recipients, $subject, $body, $text_headers); - } /** @@ -151,9 +183,9 @@ class Mail foreach ($headers as $key => $value) { if (strcasecmp($key, 'From') === 0) { include_once 'Mail/RFC822.php'; - $parser = &new Mail_RFC822(); + $parser = new Mail_RFC822(); $addresses = $parser->parseAddressList($value, 'localhost', false); - if (PEAR::isError($addresses)) { + if (is_a($addresses, 'PEAR_Error')) { return $addresses; } @@ -221,7 +253,7 @@ class Mail $addresses = Mail_RFC822::parseAddressList($recipients, 'localhost', false); // If parseAddressList() returned a PEAR_Error object, just return it. - if (PEAR::isError($addresses)) { + if (is_a($addresses, 'PEAR_Error')) { return $addresses; } diff --git a/extlib/Mail/RFC822.php b/extlib/Mail/RFC822.php old mode 100644 new mode 100755 index 8714df2e29..58d36465cb --- a/extlib/Mail/RFC822.php +++ b/extlib/Mail/RFC822.php @@ -1,37 +1,48 @@ | -// | Chuck Hagenbuch | -// +-----------------------------------------------------------------------+ +/** + * RFC 822 Email address list validation Utility + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2001-2010, Richard Heyes + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Richard Heyes + * @author Chuck Hagenbuch * @author Chuck Hagenbuch - * @version $Revision: 1.24 $ + * @version $Revision: 294749 $ * @license BSD * @package Mail */ @@ -635,8 +646,8 @@ class Mail_RFC822 { $comment = $this->_splitCheck($parts, ')'); $comments[] = $comment; - // +1 is for the trailing ) - $_mailbox = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1); + // +2 is for the brackets + $_mailbox = substr($_mailbox, strpos($_mailbox, '('.$comment)+strlen($comment)+2); } else { break; } diff --git a/extlib/Mail/mail.php b/extlib/Mail/mail.php old mode 100644 new mode 100755 index b13d695656..a8b4b5dbee --- a/extlib/Mail/mail.php +++ b/extlib/Mail/mail.php @@ -1,27 +1,52 @@ | -// +----------------------------------------------------------------------+ -// -// $Id: mail.php,v 1.20 2007/10/06 17:00:00 chagenbu Exp $ +/** + * internal PHP-mail() implementation of the PEAR Mail:: interface. + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2010 Chuck Hagenbuch + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Chuck Hagenbuch + * @copyright 2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: mail.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ /** * internal PHP-mail() implementation of the PEAR Mail:: interface. * @package Mail - * @version $Revision: 1.20 $ + * @version $Revision: 294747 $ */ class Mail_mail extends Mail { diff --git a/extlib/Mail/mock.php b/extlib/Mail/mock.php old mode 100644 new mode 100755 index 971dae6a0e..61570ba408 --- a/extlib/Mail/mock.php +++ b/extlib/Mail/mock.php @@ -1,29 +1,53 @@ | -// +----------------------------------------------------------------------+ -// -// $Id: mock.php,v 1.1 2007/12/08 17:57:54 chagenbu Exp $ -// +/** + * Mock implementation + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2010 Chuck Hagenbuch + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Chuck Hagenbuch + * @copyright 2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: mock.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ /** * Mock implementation of the PEAR Mail:: interface for testing. * @access public * @package Mail - * @version $Revision: 1.1 $ + * @version $Revision: 294747 $ */ class Mail_mock extends Mail { diff --git a/extlib/Mail/null.php b/extlib/Mail/null.php old mode 100644 new mode 100755 index 982bfa45b6..f8d58272ee --- a/extlib/Mail/null.php +++ b/extlib/Mail/null.php @@ -1,29 +1,53 @@ | -// +----------------------------------------------------------------------+ -// -// $Id: null.php,v 1.2 2004/04/06 05:19:03 jon Exp $ -// +/** + * Null implementation of the PEAR Mail interface + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2010 Phil Kernick + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail + * @author Phil Kernick + * @copyright 2010 Phil Kernick + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: null.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ /** * Null implementation of the PEAR Mail:: interface. * @access public * @package Mail - * @version $Revision: 1.2 $ + * @version $Revision: 294747 $ */ class Mail_null extends Mail { diff --git a/extlib/Mail/sendmail.php b/extlib/Mail/sendmail.php old mode 100644 new mode 100755 index cd248e61d2..b056575e99 --- a/extlib/Mail/sendmail.php +++ b/extlib/Mail/sendmail.php @@ -20,7 +20,7 @@ * Sendmail implementation of the PEAR Mail:: interface. * @access public * @package Mail - * @version $Revision: 1.19 $ + * @version $Revision: 294744 $ */ class Mail_sendmail extends Mail { @@ -117,7 +117,7 @@ class Mail_sendmail extends Mail { if (is_a($recipients, 'PEAR_Error')) { return $recipients; } - $recipients = escapeShellCmd(implode(' ', $recipients)); + $recipients = implode(' ', array_map('escapeshellarg', $recipients)); $headerElements = $this->prepareHeaders($headers); if (is_a($headerElements, 'PEAR_Error')) { @@ -141,7 +141,8 @@ class Mail_sendmail extends Mail { return PEAR::raiseError('From address specified with dangerous characters.'); } - $from = escapeShellCmd($from); + $from = escapeshellarg($from); // Security bug #16200 + $mail = @popen($this->sendmail_path . (!empty($this->sendmail_args) ? ' ' . $this->sendmail_args : '') . " -f$from -- $recipients", 'w'); if (!$mail) { return PEAR::raiseError('Failed to open sendmail [' . $this->sendmail_path . '] for execution.'); diff --git a/extlib/Mail/smtp.php b/extlib/Mail/smtp.php old mode 100644 new mode 100755 index baf3a962ba..52ea602086 --- a/extlib/Mail/smtp.php +++ b/extlib/Mail/smtp.php @@ -1,21 +1,48 @@ | -// | Jon Parise | -// +----------------------------------------------------------------------+ +/** + * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class. + * + * PHP versions 4 and 5 + * + * LICENSE: + * + * Copyright (c) 2010, Chuck Hagenbuch + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request + * @author Jon Parise + * @author Chuck Hagenbuch + * @copyright 2010 Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: smtp.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ + */ /** Error: Failed to create a Net_SMTP object */ define('PEAR_MAIL_SMTP_ERROR_CREATE', 10000); @@ -42,7 +69,7 @@ define('PEAR_MAIL_SMTP_ERROR_DATA', 10006); * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class. * @access public * @package Mail - * @version $Revision: 1.33 $ + * @version $Revision: 294747 $ */ class Mail_smtp extends Mail { @@ -278,6 +305,16 @@ class Mail_smtp extends Mail { /* Send the message's headers and the body as SMTP data. */ $res = $this->_smtp->data($textHeaders . "\r\n\r\n" . $body); + list(,$args) = $this->_smtp->getResponse(); + + if (preg_match("/Ok: queued as (.*)/", $args, $queued)) { + $this->queued_as = $queued[1]; + } + + /* we need the greeting; from it we can extract the authorative name of the mail server we've really connected to. + * ideal if we're connecting to a round-robin of relay servers and need to track which exact one took the email */ + $this->greeting = $this->_smtp->getGreeting(); + if (is_a($res, 'PEAR_Error')) { $error = $this->_error('Failed to send data', $res); $this->_smtp->rset(); diff --git a/extlib/Mail/smtpmx.php b/extlib/Mail/smtpmx.php old mode 100644 new mode 100755 index 9d2dccfb13..f0b6940868 --- a/extlib/Mail/smtpmx.php +++ b/extlib/Mail/smtpmx.php @@ -8,19 +8,43 @@ * * PHP versions 4 and 5 * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. + * LICENSE: + * + * Copyright (c) 2010, gERD Schaufelberger + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * o The names of the authors may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * @category Mail * @package Mail_smtpmx * @author gERD Schaufelberger - * @copyright 1997-2005 The PHP Group - * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version CVS: $Id: smtpmx.php,v 1.2 2007/10/06 17:00:00 chagenbu Exp $ - * @see Mail + * @copyright 2010 gERD Schaufelberger + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: smtpmx.php 294747 2010-02-08 08:18:33Z clockwerx $ + * @link http://pear.php.net/package/Mail/ */ require_once 'Net/SMTP.php'; @@ -32,7 +56,7 @@ require_once 'Net/SMTP.php'; * @access public * @author gERD Schaufelberger * @package Mail - * @version $Revision: 1.2 $ + * @version $Revision: 294747 $ */ class Mail_smtpmx extends Mail { diff --git a/extlib/Net/SMTP.php b/extlib/Net/SMTP.php index d632258d63..ea4b55e8d2 100644 --- a/extlib/Net/SMTP.php +++ b/extlib/Net/SMTP.php @@ -18,7 +18,7 @@ // | Damian Alejandro Fernandez Sosa | // +----------------------------------------------------------------------+ // -// $Id: SMTP.php,v 1.63 2008/06/10 05:39:12 jon Exp $ +// $Id: SMTP.php 293948 2010-01-24 21:46:00Z jon $ require_once 'PEAR.php'; require_once 'Net/Socket.php'; @@ -91,6 +91,13 @@ class Net_SMTP */ var $_debug = false; + /** + * Debug output handler. + * @var callback + * @access private + */ + var $_debug_handler = null; + /** * The socket resource being used to connect to the SMTP server. * @var resource @@ -112,6 +119,13 @@ class Net_SMTP */ var $_arguments = array(); + /** + * Stores the SMTP server's greeting string. + * @var string + * @access private + */ + var $_greeting = null; + /** * Stores detected features of the SMTP server. * @var array @@ -172,9 +186,30 @@ class Net_SMTP * @access public * @since 1.1.0 */ - function setDebug($debug) + function setDebug($debug, $handler = null) { $this->_debug = $debug; + $this->_debug_handler = $handler; + } + + /** + * Write the given debug text to the current debug output handler. + * + * @param string $message Debug mesage text. + * + * @access private + * @since 1.3.3 + */ + function _debug($message) + { + if ($this->_debug) { + if ($this->_debug_handler) { + call_user_func_array($this->_debug_handler, + array(&$this, $message)); + } else { + echo "DEBUG: $message\n"; + } + } } /** @@ -189,13 +224,12 @@ class Net_SMTP */ function _send($data) { - if ($this->_debug) { - echo "DEBUG: Send: $data\n"; - } + $this->_debug("Send: $data"); - if (PEAR::isError($error = $this->_socket->write($data))) { - return PEAR::raiseError('Failed to write to socket: ' . - $error->getMessage()); + $error = $this->_socket->write($data); + if ($error === false || PEAR::isError($error)) { + $msg = ($error) ? $error->getMessage() : "unknown error"; + return PEAR::raiseError("Failed to write to socket: $msg"); } return true; @@ -262,9 +296,7 @@ class Net_SMTP for ($i = 0; $i <= $this->_pipelined_commands; $i++) { while ($line = $this->_socket->readLine()) { - if ($this->_debug) { - echo "DEBUG: Recv: $line\n"; - } + $this->_debug("Recv: $line"); /* If we receive an empty line, the connection has been closed. */ if (empty($line)) { @@ -319,6 +351,20 @@ class Net_SMTP return array($this->_code, join("\n", $this->_arguments)); } + /** + * Return the SMTP server's greeting string. + * + * @return string A string containing the greeting string, or null if a + * greeting has not been received. + * + * @access public + * @since 1.3.3 + */ + function getGreeting() + { + return $this->_greeting; + } + /** * Attempt to connect to the SMTP server. * @@ -334,6 +380,7 @@ class Net_SMTP */ function connect($timeout = null, $persistent = false) { + $this->_greeting = null; $result = $this->_socket->connect($this->host, $this->port, $persistent, $timeout); if (PEAR::isError($result)) { @@ -344,6 +391,10 @@ class Net_SMTP if (PEAR::isError($error = $this->_parseResponse(220))) { return $error; } + + /* Extract and store a copy of the server's greeting string. */ + list(, $this->_greeting) = $this->getResponse(); + if (PEAR::isError($error = $this->_negotiate())) { return $error; } @@ -452,40 +503,43 @@ class Net_SMTP * @param string The password to authenticate with. * @param string The requested authentication method. If none is * specified, the best supported method will be used. + * @param bool Flag indicating whether or not TLS should be attempted. * * @return mixed Returns a PEAR_Error with an error message on any * kind of failure, or true on success. * @access public * @since 1.0 */ - function auth($uid, $pwd , $method = '') + function auth($uid, $pwd , $method = '', $tls = true) { - if (empty($this->_esmtp['AUTH'])) { - if (version_compare(PHP_VERSION, '5.1.0', '>=')) { - if (!isset($this->_esmtp['STARTTLS'])) { - return PEAR::raiseError('SMTP server does not support authentication'); - } - if (PEAR::isError($result = $this->_put('STARTTLS'))) { - return $result; - } - if (PEAR::isError($result = $this->_parseResponse(220))) { - return $result; - } - if (PEAR::isError($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT))) { - return $result; - } elseif ($result !== true) { - return PEAR::raiseError('STARTTLS failed'); - } - - /* Send EHLO again to recieve the AUTH string from the - * SMTP server. */ - $this->_negotiate(); - if (empty($this->_esmtp['AUTH'])) { - return PEAR::raiseError('SMTP server does not support authentication'); - } - } else { - return PEAR::raiseError('SMTP server does not support authentication'); + /* We can only attempt a TLS connection if one has been requested, + * we're running PHP 5.1.0 or later, have access to the OpenSSL + * extension, are connected to an SMTP server which supports the + * STARTTLS extension, and aren't already connected over a secure + * (SSL) socket connection. */ + if ($tls && version_compare(PHP_VERSION, '5.1.0', '>=') && + extension_loaded('openssl') && isset($this->_esmtp['STARTTLS']) && + strncasecmp($this->host, 'ssl://', 6) !== 0) { + /* Start the TLS connection attempt. */ + if (PEAR::isError($result = $this->_put('STARTTLS'))) { + return $result; } + if (PEAR::isError($result = $this->_parseResponse(220))) { + return $result; + } + if (PEAR::isError($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT))) { + return $result; + } elseif ($result !== true) { + return PEAR::raiseError('STARTTLS failed'); + } + + /* Send EHLO again to recieve the AUTH string from the + * SMTP server. */ + $this->_negotiate(); + } + + if (empty($this->_esmtp['AUTH'])) { + return PEAR::raiseError('SMTP server does not support authentication'); } /* If no method has been specified, get the name of the best @@ -844,30 +898,51 @@ class Net_SMTP /** * Send the DATA command. * - * @param string $data The message body to send. + * @param mixed $data The message data, either as a string or an open + * file resource. + * @param string $headers The message headers. If $headers is provided, + * $data is assumed to contain only body data. * * @return mixed Returns a PEAR_Error with an error message on any * kind of failure, or true on success. * @access public * @since 1.0 */ - function data($data) + function data($data, $headers = null) { + /* Verify that $data is a supported type. */ + if (!is_string($data) && !is_resource($data)) { + return PEAR::raiseError('Expected a string or file resource'); + } + /* RFC 1870, section 3, subsection 3 states "a value of zero * indicates that no fixed maximum message size is in force". * Furthermore, it says that if "the parameter is omitted no * information is conveyed about the server's fixed maximum * message size". */ if (isset($this->_esmtp['SIZE']) && ($this->_esmtp['SIZE'] > 0)) { - if (strlen($data) >= $this->_esmtp['SIZE']) { + /* Start by considering the size of the optional headers string. + * We also account for the addition 4 character "\r\n\r\n" + * separator sequence. */ + $size = (is_null($headers)) ? 0 : strlen($headers) + 4; + + if (is_resource($data)) { + $stat = fstat($data); + if ($stat === false) { + return PEAR::raiseError('Failed to get file size'); + } + $size += $stat['size']; + } else { + $size += strlen($data); + } + + if ($size >= $this->_esmtp['SIZE']) { $this->disconnect(); - return PEAR::raiseError('Message size excedes the server limit'); + return PEAR::raiseError('Message size exceeds server limit'); } } - /* Quote the data based on the SMTP standards. */ - $this->quotedata($data); - + /* Initiate the DATA command. */ if (PEAR::isError($error = $this->_put('DATA'))) { return $error; } @@ -875,9 +950,40 @@ class Net_SMTP return $error; } - if (PEAR::isError($result = $this->_send($data . "\r\n.\r\n"))) { - return $result; + /* If we have a separate headers string, send it first. */ + if (!is_null($headers)) { + $this->quotedata($headers); + if (PEAR::isError($result = $this->_send($headers . "\r\n\r\n"))) { + return $result; + } } + + /* Now we can send the message body data. */ + if (is_resource($data)) { + /* Stream the contents of the file resource out over our socket + * connection, line by line. Each line must be run through the + * quoting routine. */ + while ($line = fgets($data, 1024)) { + $this->quotedata($line); + if (PEAR::isError($result = $this->_send($line))) { + return $result; + } + } + + /* Finally, send the DATA terminator sequence. */ + if (PEAR::isError($result = $this->_send("\r\n.\r\n"))) { + return $result; + } + } else { + /* Just send the entire quoted string followed by the DATA + * terminator. */ + $this->quotedata($data); + if (PEAR::isError($result = $this->_send($data . "\r\n.\r\n"))) { + return $result; + } + } + + /* Verify that the data was successfully received by the server. */ if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { return $error; } From 3f9b8b293d5071357771ce7a88ffaf530ce229e8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 10 Dec 2010 22:08:36 +0000 Subject: [PATCH 02/16] Workaround for locally-handled sessions breaking on PHP 5.3 with APC enabled. Big thanks to the folks at http://pecl.php.net/bugs/bug.php?id=16745 for the secret juju! Classes were being torn down before session save handlers got called at the end of the request, which exploded with complaints about being unable to find various classes. Registering a shutdown function lets us explicitly close out the session before everything gets torn down. --- classes/Session.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/classes/Session.php b/classes/Session.php index 2422f8b68e..e1c83ad4dc 100644 --- a/classes/Session.php +++ b/classes/Session.php @@ -178,6 +178,18 @@ class Session extends Memcached_DataObject $result = session_set_save_handler('Session::open', 'Session::close', 'Session::read', 'Session::write', 'Session::destroy', 'Session::gc'); self::logdeb("save handlers result = $result"); + + // PHP 5.3 with APC ends up destroying a bunch of object stuff before the session + // save handlers get called on request teardown. + // Registering an explicit shutdown function should take care of this before + // everything breaks on us. + register_shutdown_function('Session::cleanup'); + return $result; } + + static function cleanup() + { + session_write_close(); + } } From 39cad55711897323fac5f651c003c4d815a51ae0 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 13 Dec 2010 12:12:22 -0800 Subject: [PATCH 03/16] TwitterBridge: partial merge of id_str usage from 0.9.x for improved 32-bit and pre-5.2.10 compatibility. (on 64-bit in 5.2.6 we can pull the integer IDs, but silently lose some precision on the end.) Fixes for Twitter bridge breakage on 32-bit servers. New "Snowflake" 64-bit IDs have become too big to fit in the integer portion of double-precision floats, so to reliably use these IDs we need to pull the new string form now. Machines with 64-bit PHP installation should have had no problems (except on Windows, where integers are still 32 bits) Conflicts: plugins/TwitterBridge/twitterimport.php <- as this hasn't been broken out, the import code is NOT FULLY UPDATED HERE. --- .../daemons/twitterstatusfetcher.php | 5 +-- plugins/TwitterBridge/twitter.php | 32 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php index cef67b1806..1b9cca8ecb 100755 --- a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php +++ b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php @@ -218,8 +218,9 @@ class TwitterStatusFetcher extends ParallelizingDaemon } if (!empty($timeline)) { - Twitter_synch_status::setLastId($flink->foreign_id, 'home_timeline', $timeline[0]->id); - common_debug("Set lastId value '{$timeline[0]->id}' for foreign id '{$flink->foreign_id}' and timeline 'home_timeline'"); + $lastId = twitter_id($timeline[0]); + Twitter_synch_status::setLastId($flink->foreign_id, 'home_timeline', $lastId); + common_debug("Set lastId value '$lastId' for foreign id '{$flink->foreign_id}' and timeline 'home_timeline'"); } // Okay, record the time we synced with Twitter for posterity diff --git a/plugins/TwitterBridge/twitter.php b/plugins/TwitterBridge/twitter.php index b34488069a..e8d11f3b6a 100644 --- a/plugins/TwitterBridge/twitter.php +++ b/plugins/TwitterBridge/twitter.php @@ -128,6 +128,34 @@ function is_twitter_notice($id) return (!empty($n2s)); } +/** + * Pull the formatted status ID number from a Twitter status object + * returned via JSON from Twitter API. + * + * Encapsulates checking for the id_str attribute, which is required + * to read 64-bit "Snowflake" ID numbers on a 32-bit system -- the + * integer id attribute gets corrupted into a double-precision float, + * losing a few digits of precision. + * + * Warning: avoid performing arithmetic or direct comparisons with + * this number, as it may get coerced back to a double on 32-bit. + * + * @param object $status + * @param string $field base field name if not 'id' + * @return mixed id number as int or string + */ +function twitter_id($status, $field='id') +{ + $field_str = "{$field}_str"; + if (isset($status->$field_str)) { + // String version of the id -- required on 32-bit systems + // since the 64-bit numbers get corrupted as ints. + return $status->$field_str; + } else { + return $status->$field; + } +} + /** * Check if we need to broadcast a notice over the Twitter bridge, and * do so if necessary. Will determine whether to do a straight post or @@ -148,7 +176,7 @@ function broadcast_twitter($notice) if (!empty($notice->repeat_of) && is_twitter_notice($notice->repeat_of)) { $retweet = retweet_notice($flink, Notice::staticGet('id', $notice->repeat_of)); if (is_object($retweet)) { - Notice_to_status::saveNew($notice->id, $retweet->id); + Notice_to_status::saveNew($notice->id, twitter_id($retweet)); return true; } else { // Our error processing will have decided if we need to requeue @@ -242,7 +270,7 @@ function broadcast_oauth($notice, $flink) { try { $status = $client->statusesUpdate($statustxt, $params); if (!empty($status)) { - Notice_to_status::saveNew($notice->id, $status->id); + Notice_to_status::saveNew($notice->id, twitter_id($status)); } } catch (OAuthClientException $e) { return process_error($e, $flink, $notice); From 654d1749da1f57f991dc03cab6a6787754b2c4eb Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 13 Dec 2010 17:48:23 -0800 Subject: [PATCH 04/16] partial stub file for atompub tests --- tests/atompub/atompub_test.php | 105 +++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/atompub/atompub_test.php diff --git a/tests/atompub/atompub_test.php b/tests/atompub/atompub_test.php new file mode 100644 index 0000000000..047e2cebbe --- /dev/null +++ b/tests/atompub/atompub_test.php @@ -0,0 +1,105 @@ +url = $url; + $this->user = $user; + $this->pass = $pass; + } + + /** + * @param string $baseUrl to attempt feed discovery from + * @return AtomPubClient + */ + static function discoverFromPage($baseUrl) + { + + } + + function get() + { + + } + + function post($stuff, $type='application/atom+xml;type=entry') + { + // post it up! + // optional 'Slug' header too + // .. receive .. + if ($response->getStatus() == '201') { + // yay + // MUST have a "Location" header + // if it has a Content-Location header, it MUST match Location + // and if it does, check the response body -- it should match what we posted, more or less. + } else { + throw new Exception("Expected HTTP 201 on POST, got " . $response->getStatus()); + } + } + + function put($data, $type='application/atom+xml;type=entry') + { + // PUT it up! + // must get a 200 back. + // unlike post, we don't get the location too. + } +} + +// discover the feed... + +// confirm the feed has edit links ..... ? + +$pub = new AtomPubClient($url, $user, $pass); + +// post! +$target = $pub->post($atom); + +// make sure it's accessible +// fetch $target -- should give us the atom entry +// edit URL should match + +// refetch the feed; make sure the new entry is in there +// edit URL should match + +// try editing! it should fail. +try { + $target2 = $pub->put($target, $atom2); + // FAIL! this shouldn't work. +} catch (AtomPubPermissionDeniedException $e) { + // yay +} + +// try deleting! +$pub->delete(); + +// fetch $target -- it should be gone now + +// fetch the feed again; the new entry should be gone again + + + + + +// make subscriptions +// make some posts +// make sure the posts go through or not depending on the subs +// remove subscriptions +// test that they don't go through now + +// group memberships too + + + + +// make sure we can't post to someone else's feed! +// make sure we can't delete someone else's messages +// make sure we can't create/delete someone else's subscriptions +// make sure we can't create/delete someone else's group memberships + From 3426f65736f72ede053f486b53d88d206daaaf6a Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 12:33:28 -0800 Subject: [PATCH 05/16] Mostly-implemented basic AtomPub tests --- tests/atompub/atompub_test.php | 258 ++++++++++++++++++++++++++++----- 1 file changed, 218 insertions(+), 40 deletions(-) diff --git a/tests/atompub/atompub_test.php b/tests/atompub/atompub_test.php index 047e2cebbe..ebe9485722 100644 --- a/tests/atompub/atompub_test.php +++ b/tests/atompub/atompub_test.php @@ -1,7 +1,49 @@ +#!/usr/bin/env php . + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..')); + +$shortoptions = 'n:p:'; +$longoptions = array('nickname=', 'password=', 'dry-run'); + +$helptext = << --nickname= Nickname of account to post as + -p --password= Password for account + --dry-run Skip tests that modify the site (post, delete) + +END_OF_HELP; + +require_once INSTALLDIR.'/scripts/commandline.inc'; class AtomPubClient { + public $url; + private $user, $pass; + /** * * @param string $url collection feed URL @@ -16,76 +58,212 @@ class AtomPubClient } /** - * @param string $baseUrl to attempt feed discovery from - * @return AtomPubClient + * Set up an HTTPClient with auth for our resource. + * + * @param string $method + * @return HTTPClient */ - static function discoverFromPage($baseUrl) + private function httpClient($method='GET') { - + $client = new HTTPClient($this->url, 'GET'); + // basic auth, whee + $client->setAuth($this->user, $this->password); + return $client; } function get() { - - } - - function post($stuff, $type='application/atom+xml;type=entry') - { - // post it up! - // optional 'Slug' header too - // .. receive .. - if ($response->getStatus() == '201') { - // yay - // MUST have a "Location" header - // if it has a Content-Location header, it MUST match Location - // and if it does, check the response body -- it should match what we posted, more or less. + $client = $this->httpClient('GET'); + $response = $client->send(); + if ($response->isOk()) { + return $response->getBody(); } else { - throw new Exception("Expected HTTP 201 on POST, got " . $response->getStatus()); + throw new Exception("Bogus return code: " . $response->getStatus()); } } + /** + * Create a new resource by POSTing it to the collection. + * If successful, will return the URL representing the + * canonical location of the new resource. Neat! + * + * @param string $data + * @param string $type defaults to Atom entry + * @return string URL to the created resource + * + * @throws exceptions on failure + */ + function post($data, $type='application/atom+xml;type=entry') + { + $client = $this->httpClient('POST'); + $client->setHeader('Content-Type', $type); + // optional Slug header not used in this case + $client->setBody($data); + $response = $client->send(); + + if ($response->getStatus() != '201') { + throw new Exception("Expected HTTP 201 on POST, got " . $response->getStatus()); + } + $loc = $response->getHeader('Location'); + $contentLoc = $response->getHeader('Content-Location'); + + if (empty($loc)) { + throw new Exception("AtomPub POST response missing Location header."); + } + if (!empty($contentLoc)) { + if ($loc != $contentLoc) { + throw new Exception("AtomPub POST response Location and Content-Location headers do not match."); + } + + // If Content-Location and Location match, that means the response + // body is safe to interpret as the resource itself. + if ($type == 'application/atom+xml;type=entry') { + self::validateAtomEntry($response->getBody()); + } + } + + return $loc; + } + + /** + * Note that StatusNet currently doesn't allow PUT editing on notices. + * + * @param string $data + * @param string $type defaults to Atom entry + * @return true on success + * + * @throws exceptions on failure + */ function put($data, $type='application/atom+xml;type=entry') { - // PUT it up! - // must get a 200 back. - // unlike post, we don't get the location too. + $client = $this->httpClient('PUT'); + $client->setHeader('Content-Type', $type); + $client->setBody($data); + $response = $client->send(); + + if ($response->getStatus() != '200' && $response->getStatus() != '204') { + throw new Exception("Expected HTTP 200 or 204 on PUT, got " . $response->getStatus()); + } + + return true; + } + + /** + * Delete the resource. + * + * @return true on success + * + * @throws exceptions on failure + */ + function delete() + { + $client = $this->httpClient('GET'); + $client->setBody($data); + $response = $client->send(); + + if ($response->getStatus() != '200' && $response->getStatus() != '204') { + throw new Exception("Expected HTTP 200 or 204 on DELETE, got " . $response->getStatus()); + } + + return true; + } + + /** + * Ensure that the given string is a parseable Atom entry. + * + * @param string $str + * @return boolean + * @throws Exception on invalid input + */ + static function validateAtomEntry($str) + { + if (empty($str)) { + throw new Exception('Bad Atom entry: empty'); + } + $dom = new DOMDocument; + if (!$dom->loadXML($str)) { + throw new Exception('Bad Atom entry: XML is not well formed.'); + } + + $activity = new Activity($dom); + return true; } } + +$user = get_option_value('n', 'nickname'); +$pass = get_option_value('p', 'password'); + +if (!$user) { + die("Must set a user: --nickname=\n"); +} +if (!$pass) { + die("Must set a password: --password=\n"); +} + // discover the feed... +// @fixme will this actually work? +$url = common_local_url('ApiTimelineUser', array('format' => 'atom', 'id' => $user)); + +echo "Collection URL is: $url\n"; + +$collection = new AtomPubClient($url, $user, $pass); // confirm the feed has edit links ..... ? -$pub = new AtomPubClient($url, $user, $pass); +// $atom = ''; // post! -$target = $pub->post($atom); +echo "Posting a new message... "; +$noticeUrl = $collection->post($atom); +echo "ok, got $noticeUrl\n"; -// make sure it's accessible -// fetch $target -- should give us the atom entry +echo "Fetching the new notice... "; +$notice = new AtomPubClient($noticeUrl, $user, $pass); +$body = $notice->get(); +AtomPubClient::validateAtomEntry($body); +echo "ok\n"; + +echo "Confirming new entry looks right... "; +// confirm that it actually is what we expected +// confirm it has an edit URL that matches $target +echo "NYI\n"; + +echo "Refetching the collection... "; +$feed = $collection->get(); +echo "ok\n"; + +echo "Confirming new entry is in the feed... "; +// make sure the new entry is in there // edit URL should match +echo "NYI\n"; -// refetch the feed; make sure the new entry is in there -// edit URL should match - -// try editing! it should fail. +echo "Editing notice (should fail)... "; try { - $target2 = $pub->put($target, $atom2); - // FAIL! this shouldn't work. -} catch (AtomPubPermissionDeniedException $e) { - // yay + $notice->put($target, $atom2); + die("ERROR: editing a notice should have failed.\n"); +} catch (Exception $e) { + echo "ok (failed as expected)\n"; } -// try deleting! -$pub->delete(); - -// fetch $target -- it should be gone now - -// fetch the feed again; the new entry should be gone again - +echo "Deleting notice... "; +$notice->delete(); +echo "ok\n"; +echo "Refetching deleted notice to confirm it's gone... "; +try { + $body = $notice->get(); + die("ERROR: notice should be gone now.\n"); +} catch (Exception $e) { + echo "ok\n"; +} +echo "Refetching the collection.. "; +$feed = $collection->get(); +echo "ok\n"; +echo "Confirming deleted notice is no longer in the feed... "; +echo "NYI\n"; // make subscriptions // make some posts From 56e72ec7a138b823b8094312d935c9644838a8eb Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 12:36:21 -0800 Subject: [PATCH 06/16] auth fix --- tests/atompub/atompub_test.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/atompub/atompub_test.php b/tests/atompub/atompub_test.php index ebe9485722..99a0981e5f 100644 --- a/tests/atompub/atompub_test.php +++ b/tests/atompub/atompub_test.php @@ -67,7 +67,7 @@ class AtomPubClient { $client = new HTTPClient($this->url, 'GET'); // basic auth, whee - $client->setAuth($this->user, $this->password); + $client->setAuth($this->user, $this->pass); return $client; } @@ -78,7 +78,7 @@ class AtomPubClient if ($response->isOk()) { return $response->getBody(); } else { - throw new Exception("Bogus return code: " . $response->getStatus()); + throw new Exception("Bogus return code: " . $response->getStatus() . ': ' . $response->getBody()); } } @@ -102,7 +102,7 @@ class AtomPubClient $response = $client->send(); if ($response->getStatus() != '201') { - throw new Exception("Expected HTTP 201 on POST, got " . $response->getStatus()); + throw new Exception("Expected HTTP 201 on POST, got " . $response->getStatus() . ': ' . $response->getBody()); } $loc = $response->getHeader('Location'); $contentLoc = $response->getHeader('Content-Location'); @@ -142,7 +142,7 @@ class AtomPubClient $response = $client->send(); if ($response->getStatus() != '200' && $response->getStatus() != '204') { - throw new Exception("Expected HTTP 200 or 204 on PUT, got " . $response->getStatus()); + throw new Exception("Expected HTTP 200 or 204 on PUT, got " . $response->getStatus() . ': ' . $response->getBody()); } return true; @@ -162,7 +162,7 @@ class AtomPubClient $response = $client->send(); if ($response->getStatus() != '200' && $response->getStatus() != '204') { - throw new Exception("Expected HTTP 200 or 204 on DELETE, got " . $response->getStatus()); + throw new Exception("Expected HTTP 200 or 204 on DELETE, got " . $response->getStatus() . ': ' . $response->getBody()); } return true; From 0f26d6eb70dc410bac6ec66161bf11b2f6a80fc0 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 13:11:34 -0800 Subject: [PATCH 07/16] more fixins on AtomPub tests --- tests/atompub/atompub_test.php | 36 +++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/tests/atompub/atompub_test.php b/tests/atompub/atompub_test.php index 99a0981e5f..cc1f93b44c 100644 --- a/tests/atompub/atompub_test.php +++ b/tests/atompub/atompub_test.php @@ -65,8 +65,8 @@ class AtomPubClient */ private function httpClient($method='GET') { - $client = new HTTPClient($this->url, 'GET'); - // basic auth, whee + $client = new HTTPClient($this->url); + $client->setMethod($method); $client->setAuth($this->user, $this->pass); return $client; } @@ -211,9 +211,39 @@ $collection = new AtomPubClient($url, $user, $pass); // confirm the feed has edit links ..... ? -// $atom = ''; +echo "Posting an empty message (should fail)... "; +try { + $noticeUrl = $collection->post(''); + die("FAILED, succeeded!\n"); +} catch (Exception $e) { + echo "ok\n"; +} + +echo "Posting an invalid XML message (should fail)... "; +try { + $noticeUrl = $collection->post('barf'); + die("FAILED, succeeded!\n"); +} catch (Exception $e) { + echo "ok\n"; +} + +echo "Posting a valid XML but non-Atom message (should fail)... "; +try { + $noticeUrl = $collection->post('arfbarf'); + die("FAILED, succeeded!\n"); +} catch (Exception $e) { + echo "ok\n"; +} // post! +$rand = mt_rand(0, 99999); +$atom = << + This is an AtomPub test post title ($rand) + This is an AtomPub test post content ($rand) + +END_ATOM; + echo "Posting a new message... "; $noticeUrl = $collection->post($atom); echo "ok, got $noticeUrl\n"; From 54a0e801f3485424d0ad352c02ab331b2f7a11fb Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 13:12:24 -0800 Subject: [PATCH 08/16] AtomPub fixes: return '201 Created' on POST of new message; better error checking on Atom input --- actions/apitimelineuser.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index d90507aa44..ca4b090a16 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -309,9 +309,15 @@ class ApiTimelineUserAction extends ApiBareAuthAction return; } - $xml = file_get_contents('php://input'); + $xml = trim(file_get_contents('php://input')); + if (empty($xml)) { + $this->clientError(_('Atom post must not be empty.')); + } $dom = DOMDocument::loadXML($xml); + if (!$dom) { + $this->clientError(_('Atom post must be well-formed XML.')); + } if ($dom->documentElement->namespaceURI != Activity::ATOM || $dom->documentElement->localName != 'entry') { @@ -349,6 +355,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction } if (!empty($saved)) { + header('HTTP/1.1 201 Created'); header("Location: " . common_local_url('ApiStatusesShow', array('notice_id' => $saved->id, 'format' => 'atom'))); $this->showSingleAtomStatus($saved); From 82a9560a2d11f6b9355b26f23fb1bfe224d60bfd Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 13:19:22 -0800 Subject: [PATCH 09/16] AtomPub fix: correct the response URL given from posting a new message (wrong parameter meant we got the main page instead of the message's URL) --- actions/apitimelineuser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index ca4b090a16..81809670b4 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -356,7 +356,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction if (!empty($saved)) { header('HTTP/1.1 201 Created'); - header("Location: " . common_local_url('ApiStatusesShow', array('notice_id' => $saved->id, + header("Location: " . common_local_url('ApiStatusesShow', array('id' => $saved->id, 'format' => 'atom'))); $this->showSingleAtomStatus($saved); } From 0bfeaa45594121fb89b88e4890fb2a1288a51ba6 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 13:23:09 -0800 Subject: [PATCH 10/16] AtomPub tests: fix for atom post check --- tests/atompub/atompub_test.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/atompub/atompub_test.php b/tests/atompub/atompub_test.php index cc1f93b44c..389fd12c41 100644 --- a/tests/atompub/atompub_test.php +++ b/tests/atompub/atompub_test.php @@ -185,7 +185,7 @@ class AtomPubClient throw new Exception('Bad Atom entry: XML is not well formed.'); } - $activity = new Activity($dom); + $activity = new Activity($dom->documentRoot); return true; } } @@ -283,6 +283,7 @@ echo "ok\n"; echo "Refetching deleted notice to confirm it's gone... "; try { $body = $notice->get(); + var_dump($body); die("ERROR: notice should be gone now.\n"); } catch (Exception $e) { echo "ok\n"; From 247a494006dbf276450d74a6a7c38e37ff09f8fe Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 13:25:22 -0800 Subject: [PATCH 11/16] AtomPub tests: fix delete test --- tests/atompub/atompub_test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/atompub/atompub_test.php b/tests/atompub/atompub_test.php index 389fd12c41..36a1ceec25 100644 --- a/tests/atompub/atompub_test.php +++ b/tests/atompub/atompub_test.php @@ -157,7 +157,7 @@ class AtomPubClient */ function delete() { - $client = $this->httpClient('GET'); + $client = $this->httpClient('DELETE'); $client->setBody($data); $response = $client->send(); From b7e3b06bb109a54f394311b4ba6bf3ab6a602c1b Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 13:52:44 -0800 Subject: [PATCH 12/16] AtomPub tetss: confirming edit URL linked properly in individual entry return --- tests/atompub/atompub_test.php | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/atompub/atompub_test.php b/tests/atompub/atompub_test.php index 36a1ceec25..77934c6428 100644 --- a/tests/atompub/atompub_test.php +++ b/tests/atompub/atompub_test.php @@ -188,6 +188,25 @@ class AtomPubClient $activity = new Activity($dom->documentRoot); return true; } + + static function entryEditURL($str) { + $dom = new DOMDocument; + $dom->loadXML($str); + $path = new DOMXPath($dom); + $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); + + $links = $path->query('/atom:entry/atom:link[@rel="edit"]', $dom->documentRoot); + if ($links && $links->length) { + if ($links->length > 1) { + throw new Exception('Bad Atom entry; has multiple rel=edit links.'); + } + $link = $links->item(0); + $url = $link->getAttribute('href'); + return $url; + } else { + throw new Exception('Atom entry lists no rel=edit link.'); + } + } } @@ -254,10 +273,12 @@ $body = $notice->get(); AtomPubClient::validateAtomEntry($body); echo "ok\n"; -echo "Confirming new entry looks right... "; -// confirm that it actually is what we expected -// confirm it has an edit URL that matches $target -echo "NYI\n"; +echo "Confirming new entry points to itself right... "; +$editUrl = AtomPubClient::entryEditURL($body); +if ($editUrl != $noticeUrl) { + die("Entry lists edit URL as $editUrl, no match!\n"); +} +echo "OK\n"; echo "Refetching the collection... "; $feed = $collection->get(); From fefd9056da5461d8a61f53192630281b0b57cb97 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 14:07:25 -0800 Subject: [PATCH 13/16] AtomPub test cases: make sure the posted entry appears in the feed, and that it disappears after deletion --- tests/atompub/atompub_test.php | 52 ++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/tests/atompub/atompub_test.php b/tests/atompub/atompub_test.php index 77934c6428..e23e4a711b 100644 --- a/tests/atompub/atompub_test.php +++ b/tests/atompub/atompub_test.php @@ -207,6 +207,41 @@ class AtomPubClient throw new Exception('Atom entry lists no rel=edit link.'); } } + + static function entryId($str) { + $dom = new DOMDocument; + $dom->loadXML($str); + $path = new DOMXPath($dom); + $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); + + $links = $path->query('/atom:entry/atom:id', $dom->documentRoot); + if ($links && $links->length) { + if ($links->length > 1) { + throw new Exception('Bad Atom entry; has multiple id entries.'); + } + $link = $links->item(0); + $url = $link->textContent; + return $url; + } else { + throw new Exception('Atom entry lists no id.'); + } + } + + static function getEntryInFeed($str, $id) + { + $dom = new DOMDocument; + $dom->loadXML($str); + $path = new DOMXPath($dom); + $path->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); + + $query = '/atom:feed/atom:entry[atom:id="'.$id.'"]'; + $items = $path->query($query, $dom->documentRoot); + if ($items && $items->length) { + return $items->item(0); + } else { + return null; + } + } } @@ -273,6 +308,10 @@ $body = $notice->get(); AtomPubClient::validateAtomEntry($body); echo "ok\n"; +echo "Getting the notice ID URI... "; +$noticeUri = AtomPubClient::entryId($body); +echo "ok: $noticeUri\n"; + echo "Confirming new entry points to itself right... "; $editUrl = AtomPubClient::entryEditURL($body); if ($editUrl != $noticeUrl) { @@ -285,9 +324,12 @@ $feed = $collection->get(); echo "ok\n"; echo "Confirming new entry is in the feed... "; -// make sure the new entry is in there +$entry = AtomPubClient::getEntryInFeed($feed, $noticeUri); +if (!$entry) { + die("missing!\n"); +} // edit URL should match -echo "NYI\n"; +echo "ok\n"; echo "Editing notice (should fail)... "; try { @@ -315,7 +357,11 @@ $feed = $collection->get(); echo "ok\n"; echo "Confirming deleted notice is no longer in the feed... "; -echo "NYI\n"; +$entry = AtomPubClient::getEntryInFeed($feed, $noticeUri); +if ($entry) { + die("still there!\n"); +} +echo "ok\n"; // make subscriptions // make some posts From 2ed1e9b126baa3d09cb41b1c4ea4016ae4f89936 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 14:43:50 -0800 Subject: [PATCH 14/16] AtomPub discovery fix: gets MarsEdit's auto API detection working. Router entry for AtomPubService was slightly off, generating an incorrect link in the RSD data. --- lib/router.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/router.php b/lib/router.php index c42cca5f60..24cda72b61 100644 --- a/lib/router.php +++ b/lib/router.php @@ -905,8 +905,8 @@ class Router // AtomPub API $m->connect('api/statusnet/app/service/:id.xml', - array('action' => 'ApiAtomService', - 'id' => Nickname::DISPLAY_FMT)); + array('action' => 'ApiAtomService'), + array('id' => Nickname::DISPLAY_FMT)); $m->connect('api/statusnet/app/service.xml', array('action' => 'ApiAtomService')); From 6c671141982c5837a2e5bf1e90de389c728d5dee Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 14 Dec 2010 16:14:15 -0800 Subject: [PATCH 15/16] Mark OembedAction, XrdAction, and (plugin) AutocompleteAction as read-only. Tweaked ApiStatusesShow and ApiTimelineUser to still claim read-only when hit with a HEAD request (usually link checkers or a precursor to a GET, and should be semantically equivalent to a GET without actually transferring data) --- actions/apistatusesshow.php | 8 ++------ actions/apitimelineuser.php | 8 ++------ actions/oembed.php | 11 +++++++++++ lib/xrdaction.php | 12 ++++++++++++ plugins/Autocomplete/autocomplete.php | 12 ++++++++++++ 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/actions/apistatusesshow.php b/actions/apistatusesshow.php index e684a07eec..80b0374a63 100644 --- a/actions/apistatusesshow.php +++ b/actions/apistatusesshow.php @@ -165,7 +165,7 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction } /** - * Is this action read only? + * We expose AtomPub here, so non-GET/HEAD reqs must be read/write. * * @param array $args other arguments * @@ -174,11 +174,7 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction function isReadOnly($args) { - if ($_SERVER['REQUEST_METHOD'] == 'GET') { - return true; - } else { - return false; - } + return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD'); } /** diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 81809670b4..42988a00f6 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -235,7 +235,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction } /** - * Is this action read only? + * We expose AtomPub here, so non-GET/HEAD reqs must be read/write. * * @param array $args other arguments * @@ -244,11 +244,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction function isReadOnly($args) { - if ($_SERVER['REQUEST_METHOD'] == 'GET') { - return true; - } else { - return false; - } + return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD'); } /** diff --git a/actions/oembed.php b/actions/oembed.php index 09d68a446e..bef707f92a 100644 --- a/actions/oembed.php +++ b/actions/oembed.php @@ -215,4 +215,15 @@ class OembedAction extends Action return; } + /** + * Is this action read-only? + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return true; + } } diff --git a/lib/xrdaction.php b/lib/xrdaction.php index 4377eab943..855ed1ea89 100644 --- a/lib/xrdaction.php +++ b/lib/xrdaction.php @@ -145,4 +145,16 @@ class XrdAction extends Action return (substr($uri, 0, 5) == 'acct:'); } + + /** + * Is this action read-only? + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return true; + } } diff --git a/plugins/Autocomplete/autocomplete.php b/plugins/Autocomplete/autocomplete.php index c92002245f..e15e95ec19 100644 --- a/plugins/Autocomplete/autocomplete.php +++ b/plugins/Autocomplete/autocomplete.php @@ -165,4 +165,16 @@ class AutocompleteAction extends Action print json_encode($result) . "\n"; } } + + /** + * Is this action read-only? + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return true; + } } From 0330bad688e902df7c4a6f0db7faed52b9ccfbcb Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 15 Dec 2010 12:14:25 -0800 Subject: [PATCH 16/16] Cleaner code to avoid a couple PHP notices from accessing uninitialized variables in ostatus profile discovery (these cases hit checking diaspora accounts) --- plugins/OStatus/classes/Ostatus_profile.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index b43a2b5f11..e5b8939a9d 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -1552,8 +1552,11 @@ class Ostatus_profile extends Memcached_DataObject } // Try the profile url (like foo.example.com or example.com/user/foo) - - $profileUrl = ($object->link) ? $object->link : $hints['profileurl']; + if (!empty($object->link)) { + $profileUrl = $object->link; + } else if (!empty($hints['profileurl'])) { + $profileUrl = $hints['profileurl']; + } if (!empty($profileUrl)) { $nickname = self::nicknameFromURI($profileUrl); @@ -1584,9 +1587,11 @@ class Ostatus_profile extends Memcached_DataObject protected static function nicknameFromURI($uri) { - preg_match('/(\w+):/', $uri, $matches); - - $protocol = $matches[1]; + if (preg_match('/(\w+):/', $uri, $matches)) { + $protocol = $matches[1]; + } else { + return null; + } switch ($protocol) { case 'acct':