From c62c0602feb720fb8c198ea0362c3292bab38415 Mon Sep 17 00:00:00 2001 From: Robin Millette Date: Tue, 3 Feb 2009 21:29:06 +0000 Subject: Added PEAR Services/oEmbed and its dependencies for multimedia integration. --- extlib/HTTP/Request.php | 1521 +++++++++++++++++ extlib/HTTP/Request/Listener.php | 106 ++ extlib/Net/URL2.php | 813 +++++++++ extlib/Services/oEmbed.php | 357 ++++ extlib/Services/oEmbed/Exception.php | 65 + extlib/Services/oEmbed/Exception/NoSupport.php | 63 + extlib/Services/oEmbed/Object.php | 126 ++ extlib/Services/oEmbed/Object/Common.php | 139 ++ extlib/Services/oEmbed/Object/Exception.php | 65 + extlib/Services/oEmbed/Object/Link.php | 73 + extlib/Services/oEmbed/Object/Photo.php | 89 + extlib/Services/oEmbed/Object/Rich.php | 82 + extlib/Services/oEmbed/Object/Video.php | 82 + extlib/Validate.php | 2167 ++++++++++++------------ 14 files changed, 4697 insertions(+), 1051 deletions(-) create mode 100644 extlib/HTTP/Request.php create mode 100644 extlib/HTTP/Request/Listener.php create mode 100644 extlib/Net/URL2.php create mode 100644 extlib/Services/oEmbed.php create mode 100644 extlib/Services/oEmbed/Exception.php create mode 100644 extlib/Services/oEmbed/Exception/NoSupport.php create mode 100644 extlib/Services/oEmbed/Object.php create mode 100644 extlib/Services/oEmbed/Object/Common.php create mode 100644 extlib/Services/oEmbed/Object/Exception.php create mode 100644 extlib/Services/oEmbed/Object/Link.php create mode 100644 extlib/Services/oEmbed/Object/Photo.php create mode 100644 extlib/Services/oEmbed/Object/Rich.php create mode 100644 extlib/Services/oEmbed/Object/Video.php diff --git a/extlib/HTTP/Request.php b/extlib/HTTP/Request.php new file mode 100644 index 000000000..42eac3b14 --- /dev/null +++ b/extlib/HTTP/Request.php @@ -0,0 +1,1521 @@ + + * @author Alexey Borzov + * @copyright 2002-2007 Richard Heyes + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Request.php,v 1.63 2008/10/11 11:07:10 avb Exp $ + * @link http://pear.php.net/package/HTTP_Request/ + */ + +/** + * PEAR and PEAR_Error classes (for error handling) + */ +require_once 'PEAR.php'; +/** + * Socket class + */ +require_once 'Net/Socket.php'; +/** + * URL handling class + */ +require_once 'Net/URL.php'; + +/**#@+ + * Constants for HTTP request methods + */ +define('HTTP_REQUEST_METHOD_GET', 'GET', true); +define('HTTP_REQUEST_METHOD_HEAD', 'HEAD', true); +define('HTTP_REQUEST_METHOD_POST', 'POST', true); +define('HTTP_REQUEST_METHOD_PUT', 'PUT', true); +define('HTTP_REQUEST_METHOD_DELETE', 'DELETE', true); +define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true); +define('HTTP_REQUEST_METHOD_TRACE', 'TRACE', true); +/**#@-*/ + +/**#@+ + * Constants for HTTP request error codes + */ +define('HTTP_REQUEST_ERROR_FILE', 1); +define('HTTP_REQUEST_ERROR_URL', 2); +define('HTTP_REQUEST_ERROR_PROXY', 4); +define('HTTP_REQUEST_ERROR_REDIRECTS', 8); +define('HTTP_REQUEST_ERROR_RESPONSE', 16); +define('HTTP_REQUEST_ERROR_GZIP_METHOD', 32); +define('HTTP_REQUEST_ERROR_GZIP_READ', 64); +define('HTTP_REQUEST_ERROR_GZIP_DATA', 128); +define('HTTP_REQUEST_ERROR_GZIP_CRC', 256); +/**#@-*/ + +/**#@+ + * Constants for HTTP protocol versions + */ +define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true); +define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true); +/**#@-*/ + +if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { + /** + * Whether string functions are overloaded by their mbstring equivalents + */ + define('HTTP_REQUEST_MBSTRING', true); +} else { + /** + * @ignore + */ + define('HTTP_REQUEST_MBSTRING', false); +} + +/** + * Class for performing HTTP requests + * + * Simple example (fetches yahoo.com and displays it): + * + * $a = &new HTTP_Request('http://www.yahoo.com/'); + * $a->sendRequest(); + * echo $a->getResponseBody(); + * + * + * @category HTTP + * @package HTTP_Request + * @author Richard Heyes + * @author Alexey Borzov + * @version Release: 1.4.4 + */ +class HTTP_Request +{ + /**#@+ + * @access private + */ + /** + * Instance of Net_URL + * @var Net_URL + */ + var $_url; + + /** + * Type of request + * @var string + */ + var $_method; + + /** + * HTTP Version + * @var string + */ + var $_http; + + /** + * Request headers + * @var array + */ + var $_requestHeaders; + + /** + * Basic Auth Username + * @var string + */ + var $_user; + + /** + * Basic Auth Password + * @var string + */ + var $_pass; + + /** + * Socket object + * @var Net_Socket + */ + var $_sock; + + /** + * Proxy server + * @var string + */ + var $_proxy_host; + + /** + * Proxy port + * @var integer + */ + var $_proxy_port; + + /** + * Proxy username + * @var string + */ + var $_proxy_user; + + /** + * Proxy password + * @var string + */ + var $_proxy_pass; + + /** + * Post data + * @var array + */ + var $_postData; + + /** + * Request body + * @var string + */ + var $_body; + + /** + * A list of methods that MUST NOT have a request body, per RFC 2616 + * @var array + */ + var $_bodyDisallowed = array('TRACE'); + + /** + * Methods having defined semantics for request body + * + * Content-Length header (indicating that the body follows, section 4.3 of + * RFC 2616) will be sent for these methods even if no body was added + * + * @var array + */ + var $_bodyRequired = array('POST', 'PUT'); + + /** + * Files to post + * @var array + */ + var $_postFiles = array(); + + /** + * Connection timeout. + * @var float + */ + var $_timeout; + + /** + * HTTP_Response object + * @var HTTP_Response + */ + var $_response; + + /** + * Whether to allow redirects + * @var boolean + */ + var $_allowRedirects; + + /** + * Maximum redirects allowed + * @var integer + */ + var $_maxRedirects; + + /** + * Current number of redirects + * @var integer + */ + var $_redirects; + + /** + * Whether to append brackets [] to array variables + * @var bool + */ + var $_useBrackets = true; + + /** + * Attached listeners + * @var array + */ + var $_listeners = array(); + + /** + * Whether to save response body in response object property + * @var bool + */ + var $_saveBody = true; + + /** + * Timeout for reading from socket (array(seconds, microseconds)) + * @var array + */ + var $_readTimeout = null; + + /** + * Options to pass to Net_Socket::connect. See stream_context_create + * @var array + */ + var $_socketOptions = null; + /**#@-*/ + + /** + * Constructor + * + * Sets up the object + * @param string The url to fetch/access + * @param array Associative array of parameters which can have the following keys: + *
    + *
  • method - Method to use, GET, POST etc (string)
  • + *
  • http - HTTP Version to use, 1.0 or 1.1 (string)
  • + *
  • user - Basic Auth username (string)
  • + *
  • pass - Basic Auth password (string)
  • + *
  • proxy_host - Proxy server host (string)
  • + *
  • proxy_port - Proxy server port (integer)
  • + *
  • proxy_user - Proxy auth username (string)
  • + *
  • proxy_pass - Proxy auth password (string)
  • + *
  • timeout - Connection timeout in seconds (float)
  • + *
  • allowRedirects - Whether to follow redirects or not (bool)
  • + *
  • maxRedirects - Max number of redirects to follow (integer)
  • + *
  • useBrackets - Whether to append [] to array variable names (bool)
  • + *
  • saveBody - Whether to save response body in response object property (bool)
  • + *
  • readTimeout - Timeout for reading / writing data over the socket (array (seconds, microseconds))
  • + *
  • socketOptions - Options to pass to Net_Socket object (array)
  • + *
+ * @access public + */ + function HTTP_Request($url = '', $params = array()) + { + $this->_method = HTTP_REQUEST_METHOD_GET; + $this->_http = HTTP_REQUEST_HTTP_VER_1_1; + $this->_requestHeaders = array(); + $this->_postData = array(); + $this->_body = null; + + $this->_user = null; + $this->_pass = null; + + $this->_proxy_host = null; + $this->_proxy_port = null; + $this->_proxy_user = null; + $this->_proxy_pass = null; + + $this->_allowRedirects = false; + $this->_maxRedirects = 3; + $this->_redirects = 0; + + $this->_timeout = null; + $this->_response = null; + + foreach ($params as $key => $value) { + $this->{'_' . $key} = $value; + } + + if (!empty($url)) { + $this->setURL($url); + } + + // Default useragent + $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )'); + + // We don't do keep-alives by default + $this->addHeader('Connection', 'close'); + + // Basic authentication + if (!empty($this->_user)) { + $this->addHeader('Authorization', 'Basic ' . base64_encode($this->_user . ':' . $this->_pass)); + } + + // Proxy authentication (see bug #5913) + if (!empty($this->_proxy_user)) { + $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($this->_proxy_user . ':' . $this->_proxy_pass)); + } + + // Use gzip encoding if possible + if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib')) { + $this->addHeader('Accept-Encoding', 'gzip'); + } + } + + /** + * Generates a Host header for HTTP/1.1 requests + * + * @access private + * @return string + */ + function _generateHostHeader() + { + if ($this->_url->port != 80 AND strcasecmp($this->_url->protocol, 'http') == 0) { + $host = $this->_url->host . ':' . $this->_url->port; + + } elseif ($this->_url->port != 443 AND strcasecmp($this->_url->protocol, 'https') == 0) { + $host = $this->_url->host . ':' . $this->_url->port; + + } elseif ($this->_url->port == 443 AND strcasecmp($this->_url->protocol, 'https') == 0 AND strpos($this->_url->url, ':443') !== false) { + $host = $this->_url->host . ':' . $this->_url->port; + + } else { + $host = $this->_url->host; + } + + return $host; + } + + /** + * Resets the object to its initial state (DEPRECATED). + * Takes the same parameters as the constructor. + * + * @param string $url The url to be requested + * @param array $params Associative array of parameters + * (see constructor for details) + * @access public + * @deprecated deprecated since 1.2, call the constructor if this is necessary + */ + function reset($url, $params = array()) + { + $this->HTTP_Request($url, $params); + } + + /** + * Sets the URL to be requested + * + * @param string The url to be requested + * @access public + */ + function setURL($url) + { + $this->_url = &new Net_URL($url, $this->_useBrackets); + + if (!empty($this->_url->user) || !empty($this->_url->pass)) { + $this->setBasicAuth($this->_url->user, $this->_url->pass); + } + + if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) { + $this->addHeader('Host', $this->_generateHostHeader()); + } + + // set '/' instead of empty path rather than check later (see bug #8662) + if (empty($this->_url->path)) { + $this->_url->path = '/'; + } + } + + /** + * Returns the current request URL + * + * @return string Current request URL + * @access public + */ + function getUrl() + { + return empty($this->_url)? '': $this->_url->getUrl(); + } + + /** + * Sets a proxy to be used + * + * @param string Proxy host + * @param int Proxy port + * @param string Proxy username + * @param string Proxy password + * @access public + */ + function setProxy($host, $port = 8080, $user = null, $pass = null) + { + $this->_proxy_host = $host; + $this->_proxy_port = $port; + $this->_proxy_user = $user; + $this->_proxy_pass = $pass; + + if (!empty($user)) { + $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass)); + } + } + + /** + * Sets basic authentication parameters + * + * @param string Username + * @param string Password + */ + function setBasicAuth($user, $pass) + { + $this->_user = $user; + $this->_pass = $pass; + + $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass)); + } + + /** + * Sets the method to be used, GET, POST etc. + * + * @param string Method to use. Use the defined constants for this + * @access public + */ + function setMethod($method) + { + $this->_method = $method; + } + + /** + * Sets the HTTP version to use, 1.0 or 1.1 + * + * @param string Version to use. Use the defined constants for this + * @access public + */ + function setHttpVer($http) + { + $this->_http = $http; + } + + /** + * Adds a request header + * + * @param string Header name + * @param string Header value + * @access public + */ + function addHeader($name, $value) + { + $this->_requestHeaders[strtolower($name)] = $value; + } + + /** + * Removes a request header + * + * @param string Header name to remove + * @access public + */ + function removeHeader($name) + { + if (isset($this->_requestHeaders[strtolower($name)])) { + unset($this->_requestHeaders[strtolower($name)]); + } + } + + /** + * Adds a querystring parameter + * + * @param string Querystring parameter name + * @param string Querystring parameter value + * @param bool Whether the value is already urlencoded or not, default = not + * @access public + */ + function addQueryString($name, $value, $preencoded = false) + { + $this->_url->addQueryString($name, $value, $preencoded); + } + + /** + * Sets the querystring to literally what you supply + * + * @param string The querystring data. Should be of the format foo=bar&x=y etc + * @param bool Whether data is already urlencoded or not, default = already encoded + * @access public + */ + function addRawQueryString($querystring, $preencoded = true) + { + $this->_url->addRawQueryString($querystring, $preencoded); + } + + /** + * Adds postdata items + * + * @param string Post data name + * @param string Post data value + * @param bool Whether data is already urlencoded or not, default = not + * @access public + */ + function addPostData($name, $value, $preencoded = false) + { + if ($preencoded) { + $this->_postData[$name] = $value; + } else { + $this->_postData[$name] = $this->_arrayMapRecursive('urlencode', $value); + } + } + + /** + * Recursively applies the callback function to the value + * + * @param mixed Callback function + * @param mixed Value to process + * @access private + * @return mixed Processed value + */ + function _arrayMapRecursive($callback, $value) + { + if (!is_array($value)) { + return call_user_func($callback, $value); + } else { + $map = array(); + foreach ($value as $k => $v) { + $map[$k] = $this->_arrayMapRecursive($callback, $v); + } + return $map; + } + } + + /** + * Adds a file to form-based file upload + * + * Used to emulate file upload via a HTML form. The method also sets + * Content-Type of HTTP request to 'multipart/form-data'. + * + * If you just want to send the contents of a file as the body of HTTP + * request you should use setBody() method. + * + * @access public + * @param string name of file-upload field + * @param mixed file name(s) + * @param mixed content-type(s) of file(s) being uploaded + * @return bool true on success + * @throws PEAR_Error + */ + function addFile($inputName, $fileName, $contentType = 'application/octet-stream') + { + if (!is_array($fileName) && !is_readable($fileName)) { + return PEAR::raiseError("File '{$fileName}' is not readable", HTTP_REQUEST_ERROR_FILE); + } elseif (is_array($fileName)) { + foreach ($fileName as $name) { + if (!is_readable($name)) { + return PEAR::raiseError("File '{$name}' is not readable", HTTP_REQUEST_ERROR_FILE); + } + } + } + $this->addHeader('Content-Type', 'multipart/form-data'); + $this->_postFiles[$inputName] = array( + 'name' => $fileName, + 'type' => $contentType + ); + return true; + } + + /** + * Adds raw postdata (DEPRECATED) + * + * @param string The data + * @param bool Whether data is preencoded or not, default = already encoded + * @access public + * @deprecated deprecated since 1.3.0, method setBody() should be used instead + */ + function addRawPostData($postdata, $preencoded = true) + { + $this->_body = $preencoded ? $postdata : urlencode($postdata); + } + + /** + * Sets the request body (for POST, PUT and similar requests) + * + * @param string Request body + * @access public + */ + function setBody($body) + { + $this->_body = $body; + } + + /** + * Clears any postdata that has been added (DEPRECATED). + * + * Useful for multiple request scenarios. + * + * @access public + * @deprecated deprecated since 1.2 + */ + function clearPostData() + { + $this->_postData = null; + } + + /** + * Appends a cookie to "Cookie:" header + * + * @param string $name cookie name + * @param string $value cookie value + * @access public + */ + function addCookie($name, $value) + { + $cookies = isset($this->_requestHeaders['cookie']) ? $this->_requestHeaders['cookie']. '; ' : ''; + $this->addHeader('Cookie', $cookies . $name . '=' . $value); + } + + /** + * Clears any cookies that have been added (DEPRECATED). + * + * Useful for multiple request scenarios + * + * @access public + * @deprecated deprecated since 1.2 + */ + function clearCookies() + { + $this->removeHeader('Cookie'); + } + + /** + * Sends the request + * + * @access public + * @param bool Whether to store response body in Response object property, + * set this to false if downloading a LARGE file and using a Listener + * @return mixed PEAR error on error, true otherwise + */ + function sendRequest($saveBody = true) + { + if (!is_a($this->_url, 'Net_URL')) { + return PEAR::raiseError('No URL given', HTTP_REQUEST_ERROR_URL); + } + + $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host; + $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port; + + if (strcasecmp($this->_url->protocol, 'https') == 0) { + // Bug #14127, don't try connecting to HTTPS sites without OpenSSL + if (version_compare(PHP_VERSION, '4.3.0', '<') || !extension_loaded('openssl')) { + return PEAR::raiseError('Need PHP 4.3.0 or later with OpenSSL support for https:// requests', + HTTP_REQUEST_ERROR_URL); + } elseif (isset($this->_proxy_host)) { + return PEAR::raiseError('HTTPS proxies are not supported', HTTP_REQUEST_ERROR_PROXY); + } + $host = 'ssl://' . $host; + } + + // magic quotes may fuck up file uploads and chunked response processing + $magicQuotes = ini_get('magic_quotes_runtime'); + ini_set('magic_quotes_runtime', false); + + // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive + // connection token to a proxy server... + if (isset($this->_proxy_host) && !empty($this->_requestHeaders['connection']) && + 'Keep-Alive' == $this->_requestHeaders['connection']) + { + $this->removeHeader('connection'); + } + + $keepAlive = (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && empty($this->_requestHeaders['connection'])) || + (!empty($this->_requestHeaders['connection']) && 'Keep-Alive' == $this->_requestHeaders['connection']); + $sockets = &PEAR::getStaticProperty('HTTP_Request', 'sockets'); + $sockKey = $host . ':' . $port; + unset($this->_sock); + + // There is a connected socket in the "static" property? + if ($keepAlive && !empty($sockets[$sockKey]) && + !empty($sockets[$sockKey]->fp)) + { + $this->_sock =& $sockets[$sockKey]; + $err = null; + } else { + $this->_notify('connect'); + $this->_sock =& new Net_Socket(); + $err = $this->_sock->connect($host, $port, null, $this->_timeout, $this->_socketOptions); + } + PEAR::isError($err) or $err = $this->_sock->write($this->_buildRequest()); + + if (!PEAR::isError($err)) { + if (!empty($this->_readTimeout)) { + $this->_sock->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]); + } + + $this->_notify('sentRequest'); + + // Read the response + $this->_response = &new HTTP_Response($this->_sock, $this->_listeners); + $err = $this->_response->process( + $this->_saveBody && $saveBody, + HTTP_REQUEST_METHOD_HEAD != $this->_method + ); + + if ($keepAlive) { + $keepAlive = (isset($this->_response->_headers['content-length']) + || (isset($this->_response->_headers['transfer-encoding']) + && strtolower($this->_response->_headers['transfer-encoding']) == 'chunked')); + if ($keepAlive) { + if (isset($this->_response->_headers['connection'])) { + $keepAlive = strtolower($this->_response->_headers['connection']) == 'keep-alive'; + } else { + $keepAlive = 'HTTP/'.HTTP_REQUEST_HTTP_VER_1_1 == $this->_response->_protocol; + } + } + } + } + + ini_set('magic_quotes_runtime', $magicQuotes); + + if (PEAR::isError($err)) { + return $err; + } + + if (!$keepAlive) { + $this->disconnect(); + // Store the connected socket in "static" property + } elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) { + $sockets[$sockKey] =& $this->_sock; + } + + // Check for redirection + if ( $this->_allowRedirects + AND $this->_redirects <= $this->_maxRedirects + AND $this->getResponseCode() > 300 + AND $this->getResponseCode() < 399 + AND !empty($this->_response->_headers['location'])) { + + + $redirect = $this->_response->_headers['location']; + + // Absolute URL + if (preg_match('/^https?:\/\//i', $redirect)) { + $this->_url = &new Net_URL($redirect); + $this->addHeader('Host', $this->_generateHostHeader()); + // Absolute path + } elseif ($redirect{0} == '/') { + $this->_url->path = $redirect; + + // Relative path + } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') { + if (substr($this->_url->path, -1) == '/') { + $redirect = $this->_url->path . $redirect; + } else { + $redirect = dirname($this->_url->path) . '/' . $redirect; + } + $redirect = Net_URL::resolvePath($redirect); + $this->_url->path = $redirect; + + // Filename, no path + } else { + if (substr($this->_url->path, -1) == '/') { + $redirect = $this->_url->path . $redirect; + } else { + $redirect = dirname($this->_url->path) . '/' . $redirect; + } + $this->_url->path = $redirect; + } + + $this->_redirects++; + return $this->sendRequest($saveBody); + + // Too many redirects + } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) { + return PEAR::raiseError('Too many redirects', HTTP_REQUEST_ERROR_REDIRECTS); + } + + return true; + } + + /** + * Disconnect the socket, if connected. Only useful if using Keep-Alive. + * + * @access public + */ + function disconnect() + { + if (!empty($this->_sock) && !empty($this->_sock->fp)) { + $this->_notify('disconnect'); + $this->_sock->disconnect(); + } + } + + /** + * Returns the response code + * + * @access public + * @return mixed Response code, false if not set + */ + function getResponseCode() + { + return isset($this->_response->_code) ? $this->_response->_code : false; + } + + /** + * Returns the response reason phrase + * + * @access public + * @return mixed Response reason phrase, false if not set + */ + function getResponseReason() + { + return isset($this->_response->_reason) ? $this->_response->_reason : false; + } + + /** + * Returns either the named header or all if no name given + * + * @access public + * @param string The header name to return, do not set to get all headers + * @return mixed either the value of $headername (false if header is not present) + * or an array of all headers + */ + function getResponseHeader($headername = null) + { + if (!isset($headername)) { + return isset($this->_response->_headers)? $this->_response->_headers: array(); + } else { + $headername = strtolower($headername); + return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false; + } + } + + /** + * Returns the body of the response + * + * @access public + * @return mixed response body, false if not set + */ + function getResponseBody() + { + return isset($this->_response->_body) ? $this->_response->_body : false; + } + + /** + * Returns cookies set in response + * + * @access public + * @return mixed array of response cookies, false if none are present + */ + function getResponseCookies() + { + return isset($this->_response->_cookies) ? $this->_response->_cookies : false; + } + + /** + * Builds the request string + * + * @access private + * @return string The request string + */ + function _buildRequest() + { + $separator = ini_get('arg_separator.output'); + ini_set('arg_separator.output', '&'); + $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : ''; + ini_set('arg_separator.output', $separator); + + $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : ''; + $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : ''; + $path = $this->_url->path . $querystring; + $url = $host . $port . $path; + + if (!strlen($url)) { + $url = '/'; + } + + $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n"; + + if (in_array($this->_method, $this->_bodyDisallowed) || + (0 == strlen($this->_body) && (HTTP_REQUEST_METHOD_POST != $this->_method || + (empty($this->_postData) && empty($this->_postFiles))))) + { + $this->removeHeader('Content-Type'); + } else { + if (empty($this->_requestHeaders['content-type'])) { + // Add default content-type + $this->addHeader('Content-Type', 'application/x-www-form-urlencoded'); + } elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) { + $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime()); + $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary); + } + } + + // Request Headers + if (!empty($this->_requestHeaders)) { + foreach ($this->_requestHeaders as $name => $value) { + $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); + $request .= $canonicalName . ': ' . $value . "\r\n"; + } + } + + // Method does not allow a body, simply add a final CRLF + if (in_array($this->_method, $this->_bodyDisallowed)) { + + $request .= "\r\n"; + + // Post data if it's an array + } elseif (HTTP_REQUEST_METHOD_POST == $this->_method && + (!empty($this->_postData) || !empty($this->_postFiles))) { + + // "normal" POST request + if (!isset($boundary)) { + $postdata = implode('&', array_map( + create_function('$a', 'return $a[0] . \'=\' . $a[1];'), + $this->_flattenArray('', $this->_postData) + )); + + // multipart request, probably with file uploads + } else { + $postdata = ''; + if (!empty($this->_postData)) { + $flatData = $this->_flattenArray('', $this->_postData); + foreach ($flatData as $item) { + $postdata .= '--' . $boundary . "\r\n"; + $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"'; + $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n"; + } + } + foreach ($this->_postFiles as $name => $value) { + if (is_array($value['name'])) { + $varname = $name . ($this->_useBrackets? '[]': ''); + } else { + $varname = $name; + $value['name'] = array($value['name']); + } + foreach ($value['name'] as $key => $filename) { + $fp = fopen($filename, 'r'); + $basename = basename($filename); + $type = is_array($value['type'])? @$value['type'][$key]: $value['type']; + + $postdata .= '--' . $boundary . "\r\n"; + $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"'; + $postdata .= "\r\nContent-Type: " . $type; + $postdata .= "\r\n\r\n" . fread($fp, filesize($filename)) . "\r\n"; + fclose($fp); + } + } + $postdata .= '--' . $boundary . "--\r\n"; + } + $request .= 'Content-Length: ' . + (HTTP_REQUEST_MBSTRING? mb_strlen($postdata, 'iso-8859-1'): strlen($postdata)) . + "\r\n\r\n"; + $request .= $postdata; + + // Explicitly set request body + } elseif (0 < strlen($this->_body)) { + + $request .= 'Content-Length: ' . + (HTTP_REQUEST_MBSTRING? mb_strlen($this->_body, 'iso-8859-1'): strlen($this->_body)) . + "\r\n\r\n"; + $request .= $this->_body; + + // No body: send a Content-Length header nonetheless (request #12900), + // but do that only for methods that require a body (bug #14740) + } else { + + if (in_array($this->_method, $this->_bodyRequired)) { + $request .= "Content-Length: 0\r\n"; + } + $request .= "\r\n"; + } + + return $request; + } + + /** + * Helper function to change the (probably multidimensional) associative array + * into the simple one. + * + * @param string name for item + * @param mixed item's values + * @return array array with the following items: array('item name', 'item value'); + * @access private + */ + function _flattenArray($name, $values) + { + if (!is_array($values)) { + return array(array($name, $values)); + } else { + $ret = array(); + foreach ($values as $k => $v) { + if (empty($name)) { + $newName = $k; + } elseif ($this->_useBrackets) { + $newName = $name . '[' . $k . ']'; + } else { + $newName = $name; + } + $ret = array_merge($ret, $this->_flattenArray($newName, $v)); + } + return $ret; + } + } + + + /** + * Adds a Listener to the list of listeners that are notified of + * the object's events + * + * Events sent by HTTP_Request object + * - 'connect': on connection to server + * - 'sentRequest': after the request was sent + * - 'disconnect': on disconnection from server + * + * Events sent by HTTP_Response object + * - 'gotHeaders': after receiving response headers (headers are passed in $data) + * - 'tick': on receiving a part of response body (the part is passed in $data) + * - 'gzTick': on receiving a gzip-encoded part of response body (ditto) + * - 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped) + * + * @param HTTP_Request_Listener listener to attach + * @return boolean whether the listener was successfully attached + * @access public + */ + function attach(&$listener) + { + if (!is_a($listener, 'HTTP_Request_Listener')) { + return false; + } + $this->_listeners[$listener->getId()] =& $listener; + return true; + } + + + /** + * Removes a Listener from the list of listeners + * + * @param HTTP_Request_Listener listener to detach + * @return boolean whether the listener was successfully detached + * @access public + */ + function detach(&$listener) + { + if (!is_a($listener, 'HTTP_Request_Listener') || + !isset($this->_listeners[$listener->getId()])) { + return false; + } + unset($this->_listeners[$listener->getId()]); + return true; + } + + + /** + * Notifies all registered listeners of an event. + * + * @param string Event name + * @param mixed Additional data + * @access private + * @see HTTP_Request::attach() + */ + function _notify($event, $data = null) + { + foreach (array_keys($this->_listeners) as $id) { + $this->_listeners[$id]->update($this, $event, $data); + } + } +} + + +/** + * Response class to complement the Request class + * + * @category HTTP + * @package HTTP_Request + * @author Richard Heyes + * @author Alexey Borzov + * @version Release: 1.4.4 + */ +class HTTP_Response +{ + /** + * Socket object + * @var Net_Socket + */ + var $_sock; + + /** + * Protocol + * @var string + */ + var $_protocol; + + /** + * Return code + * @var string + */ + var $_code; + + /** + * Response reason phrase + * @var string + */ + var $_reason; + + /** + * Response headers + * @var array + */ + var $_headers; + + /** + * Cookies set in response + * @var array + */ + var $_cookies; + + /** + * Response body + * @var string + */ + var $_body = ''; + + /** + * Used by _readChunked(): remaining length of the current chunk + * @var string + */ + var $_chunkLength = 0; + + /** + * Attached listeners + * @var array + */ + var $_listeners = array(); + + /** + * Bytes left to read from message-body + * @var null|int + */ + var $_toRead; + + /** + * Constructor + * + * @param Net_Socket socket to read the response from + * @param array listeners attached to request + */ + function HTTP_Response(&$sock, &$listeners) + { + $this->_sock =& $sock; + $this->_listeners =& $listeners; + } + + + /** + * Processes a HTTP response + * + * This extracts response code, headers, cookies and decodes body if it + * was encoded in some way + * + * @access public + * @param bool Whether to store response body in object property, set + * this to false if downloading a LARGE file and using a Listener. + * This is assumed to be true if body is gzip-encoded. + * @param bool Whether the response can actually have a message-body. + * Will be set to false for HEAD requests. + * @throws PEAR_Error + * @return mixed true on success, PEAR_Error in case of malformed response + */ + function process($saveBody = true, $canHaveBody = true) + { + do { + $line = $this->_sock->readLine(); + if (!preg_match('!^(HTTP/\d\.\d) (\d{3})(?: (.+))?!', $line, $s)) { + return PEAR::raiseError('Malformed response', HTTP_REQUEST_ERROR_RESPONSE); + } else { + $this->_protocol = $s[1]; + $this->_code = intval($s[2]); + $this->_reason = empty($s[3])? null: $s[3]; + } + while ('' !== ($header = $this->_sock->readLine())) { + $this->_processHeader($header); + } + } while (100 == $this->_code); + + $this->_notify('gotHeaders', $this->_headers); + + // RFC 2616, section 4.4: + // 1. Any response message which "MUST NOT" include a message-body ... + // is always terminated by the first empty line after the header fields + // 3. ... If a message is received with both a + // Transfer-Encoding header field and a Content-Length header field, + // the latter MUST be ignored. + $canHaveBody = $canHaveBody && $this->_code >= 200 && + $this->_code != 204 && $this->_code != 304; + + // If response body is present, read it and decode + $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']); + $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']); + $hasBody = false; + if ($canHaveBody && ($chunked || !isset($this->_headers['content-length']) || + 0 != $this->_headers['content-length'])) + { + if ($chunked || !isset($this->_headers['content-length'])) { + $this->_toRead = null; + } else { + $this->_toRead = $this->_headers['content-length']; + } + while (!$this->_sock->eof() && (is_null($this->_toRead) || 0 < $this->_toRead)) { + if ($chunked) { + $data = $this->_readChunked(); + } elseif (is_null($this->_toRead)) { + $data = $this->_sock->read(4096); + } else { + $data = $this->_sock->read(min(4096, $this->_toRead)); + $this->_toRead -= HTTP_REQUEST_MBSTRING? mb_strlen($data, 'iso-8859-1'): strlen($data); + } + if ('' == $data && (!$this->_chunkLength || $this->_sock->eof())) { + break; + } else { + $hasBody = true; + if ($saveBody || $gzipped) { + $this->_body .= $data; + } + $this->_notify($gzipped? 'gzTick': 'tick', $data); + } + } + } + + if ($hasBody) { + // Uncompress the body if needed + if ($gzipped) { + $body = $this->_decodeGzip($this->_body); + if (PEAR::isError($body)) { + return $body; + } + $this->_body = $body; + $this->_notify('gotBody', $this->_body); + } else { + $this->_notify('gotBody'); + } + } + return true; + } + + + /** + * Processes the response header + * + * @access private + * @param string HTTP header + */ + function _processHeader($header) + { + if (false === strpos($header, ':')) { + return; + } + list($headername, $headervalue) = explode(':', $header, 2); + $headername = strtolower($headername); + $headervalue = ltrim($headervalue); + + if ('set-cookie' != $headername) { + if (isset($this->_headers[$headername])) { + $this->_headers[$headername] .= ',' . $headervalue; + } else { + $this->_headers[$headername] = $headervalue; + } + } else { + $this->_parseCookie($headervalue); + } + } + + + /** + * Parse a Set-Cookie header to fill $_cookies array + * + * @access private + * @param string value of Set-Cookie header + */ + function _parseCookie($headervalue) + { + $cookie = array( + 'expires' => null, + 'domain' => null, + 'path' => null, + 'secure' => false + ); + + // Only a name=value pair + if (!strpos($headervalue, ';')) { + $pos = strpos($headervalue, '='); + $cookie['name'] = trim(substr($headervalue, 0, $pos)); + $cookie['value'] = trim(substr($headervalue, $pos + 1)); + + // Some optional parameters are supplied + } else { + $elements = explode(';', $headervalue); + $pos = strpos($elements[0], '='); + $cookie['name'] = trim(substr($elements[0], 0, $pos)); + $cookie['value'] = trim(substr($elements[0], $pos + 1)); + + for ($i = 1; $i < count($elements); $i++) { + if (false === strpos($elements[$i], '=')) { + $elName = trim($elements[$i]); + $elValue = null; + } else { + list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i])); + } + $elName = strtolower($elName); + if ('secure' == $elName) { + $cookie['secure'] = true; + } elseif ('expires' == $elName) { + $cookie['expires'] = str_replace('"', '', $elValue); + } elseif ('path' == $elName || 'domain' == $elName) { + $cookie[$elName] = urldecode($elValue); + } else { + $cookie[$elName] = $elValue; + } + } + } + $this->_cookies[] = $cookie; + } + + + /** + * Read a part of response body encoded with chunked Transfer-Encoding + * + * @access private + * @return string + */ + function _readChunked() + { + // at start of the next chunk? + if (0 == $this->_chunkLength) { + $line = $this->_sock->readLine(); + if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) { + $this->_chunkLength = hexdec($matches[1]); + // Chunk with zero length indicates the end + if (0 == $this->_chunkLength) { + $this->_sock->readLine(); // make this an eof() + return ''; + } + } else { + return ''; + } + } + $data = $this->_sock->read($this->_chunkLength); + $this->_chunkLength -= HTTP_REQUEST_MBSTRING? mb_strlen($data, 'iso-8859-1'): strlen($data); + if (0 == $this->_chunkLength) { + $this->_sock->readLine(); // Trailing CRLF + } + return $data; + } + + + /** + * Notifies all registered listeners of an event. + * + * @param string Event name + * @param mixed Additional data + * @access private + * @see HTTP_Request::_notify() + */ + function _notify($event, $data = null) + { + foreach (array_keys($this->_listeners) as $id) { + $this->_listeners[$id]->update($this, $event, $data); + } + } + + + /** + * Decodes the message-body encoded by gzip + * + * The real decoding work is done by gzinflate() built-in function, this + * method only parses the header and checks data for compliance with + * RFC 1952 + * + * @access private + * @param string gzip-encoded data + * @return string decoded data + */ + function _decodeGzip($data) + { + if (HTTP_REQUEST_MBSTRING) { + $oldEncoding = mb_internal_encoding(); + mb_internal_encoding('iso-8859-1'); + } + $length = strlen($data); + // If it doesn't look like gzip-encoded data, don't bother + if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) { + return $data; + } + $method = ord(substr($data, 2, 1)); + if (8 != $method) { + return PEAR::raiseError('_decodeGzip(): unknown compression method', HTTP_REQUEST_ERROR_GZIP_METHOD); + } + $flags = ord(substr($data, 3, 1)); + if ($flags & 224) { + return PEAR::raiseError('_decodeGzip(): reserved bits are set', HTTP_REQUEST_ERROR_GZIP_DATA); + } + + // header is 10 bytes minimum. may be longer, though. + $headerLength = 10; + // extra fields, need to skip 'em + if ($flags & 4) { + if ($length - $headerLength - 2 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); + } + $extraLength = unpack('v', substr($data, 10, 2)); + if ($length - $headerLength - 2 - $extraLength[1] < 8) { + return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); + } + $headerLength += $extraLength[1] + 2; + } + // file name, need to skip that + if ($flags & 8) { + if ($length - $headerLength - 1 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); + } + $filenameLength = strpos(substr($data, $headerLength), chr(0)); + if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); + } + $headerLength += $filenameLength + 1; + } + // comment, need to skip that also + if ($flags & 16) { + if ($length - $headerLength - 1 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); + } + $commentLength = strpos(substr($data, $headerLength), chr(0)); + if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); + } + $headerLength += $commentLength + 1; + } + // have a CRC for header. let's check + if ($flags & 1) { + if ($length - $headerLength - 2 < 8) { + return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); + } + $crcReal = 0xffff & crc32(substr($data, 0, $headerLength)); + $crcStored = unpack('v', substr($data, $headerLength, 2)); + if ($crcReal != $crcStored[1]) { + return PEAR::raiseError('_decodeGzip(): header CRC check failed', HTTP_REQUEST_ERROR_GZIP_CRC); + } + $headerLength += 2; + } + // unpacked data CRC and size at the end of encoded data + $tmp = unpack('V2', substr($data, -8)); + $dataCrc = $tmp[1]; + $dataSize = $tmp[2]; + + // finally, call the gzinflate() function + // don't pass $dataSize to gzinflate, see bugs #13135, #14370 + $unpacked = gzinflate(substr($data, $headerLength, -8)); + if (false === $unpacked) { + return PEAR::raiseError('_decodeGzip(): gzinflate() call failed', HTTP_REQUEST_ERROR_GZIP_READ); + } elseif ($dataSize != strlen($unpacked)) { + return PEAR::raiseError('_decodeGzip(): data size check failed', HTTP_REQUEST_ERROR_GZIP_READ); + } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) { + return PEAR::raiseError('_decodeGzip(): data CRC check failed', HTTP_REQUEST_ERROR_GZIP_CRC); + } + if (HTTP_REQUEST_MBSTRING) { + mb_internal_encoding($oldEncoding); + } + return $unpacked; + } +} // End class HTTP_Response +?> diff --git a/extlib/HTTP/Request/Listener.php b/extlib/HTTP/Request/Listener.php new file mode 100644 index 000000000..b4fe444b3 --- /dev/null +++ b/extlib/HTTP/Request/Listener.php @@ -0,0 +1,106 @@ + + * @copyright 2002-2007 Richard Heyes + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Listener.php,v 1.3 2007/05/18 10:33:31 avb Exp $ + * @link http://pear.php.net/package/HTTP_Request/ + */ + +/** + * Listener for HTTP_Request and HTTP_Response objects + * + * This class implements the Observer part of a Subject-Observer + * design pattern. + * + * @category HTTP + * @package HTTP_Request + * @author Alexey Borzov + * @version Release: 1.4.4 + */ +class HTTP_Request_Listener +{ + /** + * A listener's identifier + * @var string + */ + var $_id; + + /** + * Constructor, sets the object's identifier + * + * @access public + */ + function HTTP_Request_Listener() + { + $this->_id = md5(uniqid('http_request_', 1)); + } + + + /** + * Returns the listener's identifier + * + * @access public + * @return string + */ + function getId() + { + return $this->_id; + } + + + /** + * This method is called when Listener is notified of an event + * + * @access public + * @param object an object the listener is attached to + * @param string Event name + * @param mixed Additional data + * @abstract + */ + function update(&$subject, $event, $data = null) + { + echo "Notified of event: '$event'\n"; + if (null !== $data) { + echo "Additional data: "; + var_dump($data); + } + } +} +?> diff --git a/extlib/Net/URL2.php b/extlib/Net/URL2.php new file mode 100644 index 000000000..7a654aed8 --- /dev/null +++ b/extlib/Net/URL2.php @@ -0,0 +1,813 @@ + | +// +-----------------------------------------------------------------------+ +// +// $Id: URL2.php,v 1.10 2008/04/26 21:57:08 schmidt Exp $ +// +// Net_URL2 Class (PHP5 Only) + +// This code is released under the BSD License - http://www.opensource.org/licenses/bsd-license.php +/** + * @license BSD License + */ +class Net_URL2 +{ + /** + * Do strict parsing in resolve() (see RFC 3986, section 5.2.2). Default + * is true. + */ + const OPTION_STRICT = 'strict'; + + /** + * Represent arrays in query using PHP's [] notation. Default is true. + */ + const OPTION_USE_BRACKETS = 'use_brackets'; + + /** + * URL-encode query variable keys. Default is true. + */ + const OPTION_ENCODE_KEYS = 'encode_keys'; + + /** + * Query variable separators when parsing the query string. Every character + * is considered a separator. Default is specified by the + * arg_separator.input php.ini setting (this defaults to "&"). + */ + const OPTION_SEPARATOR_INPUT = 'input_separator'; + + /** + * Query variable separator used when generating the query string. Default + * is specified by the arg_separator.output php.ini setting (this defaults + * to "&"). + */ + const OPTION_SEPARATOR_OUTPUT = 'output_separator'; + + /** + * Default options corresponds to how PHP handles $_GET. + */ + private $options = array( + self::OPTION_STRICT => true, + self::OPTION_USE_BRACKETS => true, + self::OPTION_ENCODE_KEYS => true, + self::OPTION_SEPARATOR_INPUT => 'x&', + self::OPTION_SEPARATOR_OUTPUT => 'x&', + ); + + /** + * @var string|bool + */ + private $scheme = false; + + /** + * @var string|bool + */ + private $userinfo = false; + + /** + * @var string|bool + */ + private $host = false; + + /** + * @var int|bool + */ + private $port = false; + + /** + * @var string + */ + private $path = ''; + + /** + * @var string|bool + */ + private $query = false; + + /** + * @var string|bool + */ + private $fragment = false; + + /** + * @param string $url an absolute or relative URL + * @param array $options + */ + public function __construct($url, $options = null) + { + $this->setOption(self::OPTION_SEPARATOR_INPUT, + ini_get('arg_separator.input')); + $this->setOption(self::OPTION_SEPARATOR_OUTPUT, + ini_get('arg_separator.output')); + if (is_array($options)) { + foreach ($options as $optionName => $value) { + $this->setOption($optionName); + } + } + + if (preg_match('@^([a-z][a-z0-9.+-]*):@i', $url, $reg)) { + $this->scheme = $reg[1]; + $url = substr($url, strlen($reg[0])); + } + + if (preg_match('@^//([^/#?]+)@', $url, $reg)) { + $this->setAuthority($reg[1]); + $url = substr($url, strlen($reg[0])); + } + + $i = strcspn($url, '?#'); + $this->path = substr($url, 0, $i); + $url = substr($url, $i); + + if (preg_match('@^\?([^#]*)@', $url, $reg)) { + $this->query = $reg[1]; + $url = substr($url, strlen($reg[0])); + } + + if ($url) { + $this->fragment = substr($url, 1); + } + } + + /** + * Returns the scheme, e.g. "http" or "urn", or false if there is no + * scheme specified, i.e. if this is a relative URL. + * + * @return string|bool + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * @param string|bool $scheme + * + * @return void + * @see getScheme() + */ + public function setScheme($scheme) + { + $this->scheme = $scheme; + } + + /** + * Returns the user part of the userinfo part (the part preceding the first + * ":"), or false if there is no userinfo part. + * + * @return string|bool + */ + public function getUser() + { + return $this->userinfo !== false ? preg_replace('@:.*$@', '', $this->userinfo) : false; + } + + /** + * Returns the password part of the userinfo part (the part after the first + * ":"), or false if there is no userinfo part (i.e. the URL does not + * contain "@" in front of the hostname) or the userinfo part does not + * contain ":". + * + * @return string|bool + */ + public function getPassword() + { + return $this->userinfo !== false ? substr(strstr($this->userinfo, ':'), 1) : false; + } + + /** + * Returns the userinfo part, or false if there is none, i.e. if the + * authority part does not contain "@". + * + * @return string|bool + */ + public function getUserinfo() + { + return $this->userinfo; + } + + /** + * Sets the userinfo part. If two arguments are passed, they are combined + * in the userinfo part as username ":" password. + * + * @param string|bool $userinfo userinfo or username + * @param string|bool $password + * + * @return void + */ + public function setUserinfo($userinfo, $password = false) + { + $this->userinfo = $userinfo; + if ($password !== false) { + $this->userinfo .= ':' . $password; + } + } + + /** + * Returns the host part, or false if there is no authority part, e.g. + * relative URLs. + * + * @return string|bool + */ + public function getHost() + { + return $this->host; + } + + /** + * @param string|bool $host + * + * @return void + */ + public function setHost($host) + { + $this->host = $host; + } + + /** + * Returns the port number, or false if there is no port number specified, + * i.e. if the default port is to be used. + * + * @return int|bool + */ + public function getPort() + { + return $this->port; + } + + /** + * @param int|bool $port + * + * @return void + */ + public function setPort($port) + { + $this->port = intval($port); + } + + /** + * Returns the authority part, i.e. [ userinfo "@" ] host [ ":" port ], or + * false if there is no authority none. + * + * @return string|bool + */ + public function getAuthority() + { + if (!$this->host) { + return false; + } + + $authority = ''; + + if ($this->userinfo !== false) { + $authority .= $this->userinfo . '@'; + } + + $authority .= $this->host; + + if ($this->port !== false) { + $authority .= ':' . $this->port; + } + + return $authority; + } + + /** + * @param string|false $authority + * + * @return void + */ + public function setAuthority($authority) + { + $this->user = false; + $this->pass = false; + $this->host = false; + $this->port = false; + if (preg_match('@^(([^\@]+)\@)?([^:]+)(:(\d*))?$@', $authority, $reg)) { + if ($reg[1]) { + $this->userinfo = $reg[2]; + } + + $this->host = $reg[3]; + if (isset($reg[5])) { + $this->port = intval($reg[5]); + } + } + } + + /** + * Returns the path part (possibly an empty string). + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * @param string $path + * + * @return void + */ + public function setPath($path) + { + $this->path = $path; + } + + /** + * Returns the query string (excluding the leading "?"), or false if "?" + * isn't present in the URL. + * + * @return string|bool + * @see self::getQueryVariables() + */ + public function getQuery() + { + return $this->query; + } + + /** + * @param string|bool $query + * + * @return void + * @see self::setQueryVariables() + */ + public function setQuery($query) + { + $this->query = $query; + } + + /** + * Returns the fragment name, or false if "#" isn't present in the URL. + * + * @return string|bool + */ + public function getFragment() + { + return $this->fragment; + } + + /** + * @param string|bool $fragment + * + * @return void + */ + public function setFragment($fragment) + { + $this->fragment = $fragment; + } + + /** + * Returns the query string like an array as the variables would appear in + * $_GET in a PHP script. + * + * @return array + */ + public function getQueryVariables() + { + $pattern = '/[' . + preg_quote($this->getOption(self::OPTION_SEPARATOR_INPUT), '/') . + ']/'; + $parts = preg_split($pattern, $this->query, -1, PREG_SPLIT_NO_EMPTY); + $return = array(); + + foreach ($parts as $part) { + if (strpos($part, '=') !== false) { + list($key, $value) = explode('=', $part, 2); + } else { + $key = $part; + $value = null; + } + + if ($this->getOption(self::OPTION_ENCODE_KEYS)) { + $key = rawurldecode($key); + } + $value = rawurldecode($value); + + if ($this->getOption(self::OPTION_USE_BRACKETS) && + preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $key, $matches)) { + + $key = $matches[1]; + $idx = $matches[2]; + + // Ensure is an array + if (empty($return[$key]) || !is_array($return[$key])) { + $return[$key] = array(); + } + + // Add data + if ($idx === '') { + $return[$key][] = $value; + } else { + $return[$key][$idx] = $value; + } + } elseif (!$this->getOption(self::OPTION_USE_BRACKETS) + && !empty($return[$key]) + ) { + $return[$key] = (array) $return[$key]; + $return[$key][] = $value; + } else { + $return[$key] = $value; + } + } + + return $return; + } + + /** + * @param array $array (name => value) array + * + * @return void + */ + public function setQueryVariables(array $array) + { + if (!$array) { + $this->query = false; + } else { + foreach ($array as $name => $value) { + if ($this->getOption(self::OPTION_ENCODE_KEYS)) { + $name = rawurlencode($name); + } + + if (is_array($value)) { + foreach ($value as $k => $v) { + $parts[] = $this->getOption(self::OPTION_USE_BRACKETS) + ? sprintf('%s[%s]=%s', $name, $k, $v) + : ($name . '=' . $v); + } + } elseif (!is_null($value)) { + $parts[] = $name . '=' . $value; + } else { + $parts[] = $name; + } + } + $this->query = implode($this->getOption(self::OPTION_SEPARATOR_OUTPUT), + $parts); + } + } + + /** + * @param string $name + * @param mixed $value + * + * @return array + */ + public function setQueryVariable($name, $value) + { + $array = $this->getQueryVariables(); + $array[$name] = $value; + $this->setQueryVariables($array); + } + + /** + * @param string $name + * + * @return void + */ + public function unsetQueryVariable($name) + { + $array = $this->getQueryVariables(); + unset($array[$name]); + $this->setQueryVariables($array); + } + + /** + * Returns a string representation of this URL. + * + * @return string + */ + public function getURL() + { + // See RFC 3986, section 5.3 + $url = ""; + + if ($this->scheme !== false) { + $url .= $this->scheme . ':'; + } + + $authority = $this->getAuthority(); + if ($authority !== false) { + $url .= '//' . $authority; + } + $url .= $this->path; + + if ($this->query !== false) { + $url .= '?' . $this->query; + } + + if ($this->fragment !== false) { + $url .= '#' . $this->fragment; + } + + return $url; + } + + /** + * Returns a normalized string representation of this URL. This is useful + * for comparison of URLs. + * + * @return string + */ + public function getNormalizedURL() + { + $url = clone $this; + $url->normalize(); + return $url->getUrl(); + } + + /** + * Returns a normalized Net_URL2 instance. + * + * @return Net_URL2 + */ + public function normalize() + { + // See RFC 3886, section 6 + + // Schemes are case-insensitive + if ($this->scheme) { + $this->scheme = strtolower($this->scheme); + } + + // Hostnames are case-insensitive + if ($this->host) { + $this->host = strtolower($this->host); + } + + // Remove default port number for known schemes (RFC 3986, section 6.2.3) + if ($this->port && + $this->scheme && + $this->port == getservbyname($this->scheme, 'tcp')) { + + $this->port = false; + } + + // Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1) + foreach (array('userinfo', 'host', 'path') as $part) { + if ($this->$part) { + $this->$part = preg_replace('/%[0-9a-f]{2}/ie', 'strtoupper("\0")', $this->$part); + } + } + + // Path segment normalization (RFC 3986, section 6.2.2.3) + $this->path = self::removeDotSegments($this->path); + + // Scheme based normalization (RFC 3986, section 6.2.3) + if ($this->host && !$this->path) { + $this->path = '/'; + } + } + + /** + * Returns whether this instance represents an absolute URL. + * + * @return bool + */ + public function isAbsolute() + { + return (bool) $this->scheme; + } + + /** + * Returns an Net_URL2 instance representing an absolute URL relative to + * this URL. + * + * @param Net_URL2|string $reference relative URL + * + * @return Net_URL2 + */ + public function resolve($reference) + { + if (is_string($reference)) { + $reference = new self($reference); + } + if (!$this->isAbsolute()) { + throw new Exception('Base-URL must be absolute'); + } + + // A non-strict parser may ignore a scheme in the reference if it is + // identical to the base URI's scheme. + if (!$this->getOption(self::OPTION_STRICT) && $reference->scheme == $this->scheme) { + $reference->scheme = false; + } + + $target = new self(''); + if ($reference->scheme !== false) { + $target->scheme = $reference->scheme; + $target->setAuthority($reference->getAuthority()); + $target->path = self::removeDotSegments($reference->path); + $target->query = $reference->query; + } else { + $authority = $reference->getAuthority(); + if ($authority !== false) { + $target->setAuthority($authority); + $target->path = self::removeDotSegments($reference->path); + $target->query = $reference->query; + } else { + if ($reference->path == '') { + $target->path = $this->path; + if ($reference->query !== false) { + $target->query = $reference->query; + } else { + $target->query = $this->query; + } + } else { + if (substr($reference->path, 0, 1) == '/') { + $target->path = self::removeDotSegments($reference->path); + } else { + // Merge paths (RFC 3986, section 5.2.3) + if ($this->host !== false && $this->path == '') { + $target->path = '/' . $this->path; + } else { + $i = strrpos($this->path, '/'); + if ($i !== false) { + $target->path = substr($this->path, 0, $i + 1); + } + $target->path .= $reference->path; + } + $target->path = self::removeDotSegments($target->path); + } + $target->query = $reference->query; + } + $target->setAuthority($this->getAuthority()); + } + $target->scheme = $this->scheme; + } + + $target->fragment = $reference->fragment; + + return $target; + } + + /** + * Removes dots as described in RFC 3986, section 5.2.4, e.g. + * "/foo/../bar/baz" => "/bar/baz" + * + * @param string $path a path + * + * @return string a path + */ + private static function removeDotSegments($path) + { + $output = ''; + + // Make sure not to be trapped in an infinite loop due to a bug in this + // method + $j = 0; + while ($path && $j++ < 100) { + // Step A + if (substr($path, 0, 2) == './') { + $path = substr($path, 2); + } elseif (substr($path, 0, 3) == '../') { + $path = substr($path, 3); + + // Step B + } elseif (substr($path, 0, 3) == '/./' || $path == '/.') { + $path = '/' . substr($path, 3); + + // Step C + } elseif (substr($path, 0, 4) == '/../' || $path == '/..') { + $path = '/' . substr($path, 4); + $i = strrpos($output, '/'); + $output = $i === false ? '' : substr($output, 0, $i); + + // Step D + } elseif ($path == '.' || $path == '..') { + $path = ''; + + // Step E + } else { + $i = strpos($path, '/'); + if ($i === 0) { + $i = strpos($path, '/', 1); + } + if ($i === false) { + $i = strlen($path); + } + $output .= substr($path, 0, $i); + $path = substr($path, $i); + } + } + + return $output; + } + + /** + * Returns a Net_URL2 instance representing the canonical URL of the + * currently executing PHP script. + * + * @return string + */ + public static function getCanonical() + { + if (!isset($_SERVER['REQUEST_METHOD'])) { + // ALERT - no current URL + throw new Exception('Script was not called through a webserver'); + } + + // Begin with a relative URL + $url = new self($_SERVER['PHP_SELF']); + $url->scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http'; + $url->host = $_SERVER['SERVER_NAME']; + $port = intval($_SERVER['SERVER_PORT']); + if ($url->scheme == 'http' && $port != 80 || + $url->scheme == 'https' && $port != 443) { + + $url->port = $port; + } + return $url; + } + + /** + * Returns the URL used to retrieve the current request. + * + * @return string + */ + public static function getRequestedURL() + { + return self::getRequested()->getUrl(); + } + + /** + * Returns a Net_URL2 instance representing the URL used to retrieve the + * current request. + * + * @return Net_URL2 + */ + public static function getRequested() + { + if (!isset($_SERVER['REQUEST_METHOD'])) { + // ALERT - no current URL + throw new Exception('Script was not called through a webserver'); + } + + // Begin with a relative URL + $url = new self($_SERVER['REQUEST_URI']); + $url->scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http'; + // Set host and possibly port + $url->setAuthority($_SERVER['HTTP_HOST']); + return $url; + } + + /** + * Sets the specified option. + * + * @param string $optionName a self::OPTION_ constant + * @param mixed $value option value + * + * @return void + * @see self::OPTION_STRICT + * @see self::OPTION_USE_BRACKETS + * @see self::OPTION_ENCODE_KEYS + */ + function setOption($optionName, $value) + { + if (!array_key_exists($optionName, $this->options)) { + return false; + } + $this->options[$optionName] = $value; + } + + /** + * Returns the value of the specified option. + * + * @param string $optionName The name of the option to retrieve + * + * @return mixed + */ + function getOption($optionName) + { + return isset($this->options[$optionName]) + ? $this->options[$optionName] : false; + } +} diff --git a/extlib/Services/oEmbed.php b/extlib/Services/oEmbed.php new file mode 100644 index 000000000..5d38ed883 --- /dev/null +++ b/extlib/Services/oEmbed.php @@ -0,0 +1,357 @@ + + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version SVN: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ + +require_once 'Validate.php'; +require_once 'Net/URL2.php'; +require_once 'HTTP/Request.php'; +require_once 'Services/oEmbed/Exception.php'; +require_once 'Services/oEmbed/Exception/NoSupport.php'; +require_once 'Services/oEmbed/Object.php'; + +/** + * Base class for consuming oEmbed objects + * + * + * 'http://www.flickr.com/services/oembed/' + * )); + * $object = $oEmbed->getObject(); + * + * // All of the objects have somewhat sane __toString() methods that allow + * // you to output them directly. + * echo (string)$object; + * + * ?> + * + * + * @category Services + * @package Services_oEmbed + * @author Joe Stump + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version Release: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ +class Services_oEmbed +{ + /** + * HTTP timeout in seconds + * + * All HTTP requests made by Services_oEmbed will respect this timeout. + * This can be passed to {@link Services_oEmbed::setOption()} or to the + * options parameter in {@link Services_oEmbed::__construct()}. + * + * @var string OPTION_TIMEOUT Timeout in seconds + */ + const OPTION_TIMEOUT = 'http_timeout'; + + /** + * HTTP User-Agent + * + * All HTTP requests made by Services_oEmbed will be sent with the + * string set by this option. + * + * @var string OPTION_USER_AGENT The HTTP User-Agent string + */ + const OPTION_USER_AGENT = 'http_user_agent'; + + /** + * The API's URI + * + * If the API is known ahead of time this option can be used to explicitly + * set it. If not present then the API is attempted to be discovered + * through the auto-discovery mechanism. + * + * @var string OPTION_API + */ + const OPTION_API = 'oembed_api'; + + /** + * Options for oEmbed requests + * + * @var array $options The options for making requests + */ + protected $options = array( + self::OPTION_TIMEOUT => 3, + self::OPTION_API => null, + self::OPTION_USER_AGENT => 'Services_oEmbed 0.1.0' + ); + + /** + * URL of object to get embed information for + * + * @var object $url {@link Net_URL2} instance of URL of object + */ + protected $url = null; + + /** + * Constructor + * + * @param string $url The URL to fetch an oEmbed for + * @param array $options A list of options for the oEmbed lookup + * + * @throws {@link Services_oEmbed_Exception} if the $url is invalid + * @throws {@link Services_oEmbed_Exception} when no valid API is found + * @return void + */ + public function __construct($url, array $options = array()) + { + if (Validate::uri($url)) { + $this->url = new Net_URL2($url); + } else { + throw new Services_oEmbed_Exception('URL is invalid'); + } + + if (count($options)) { + foreach ($options as $key => $val) { + $this->setOption($key, $val); + } + } + + if ($this->options[self::OPTION_API] === null) { + $this->options[self::OPTION_API] = $this->discover(); + } + } + + /** + * Set an option for the oEmbed request + * + * @param mixed $option The option name + * @param mixed $value The option value + * + * @see Services_oEmbed::OPTION_API, Services_oEmbed::OPTION_TIMEOUT + * @throws {@link Services_oEmbed_Exception} on invalid option + * @access public + * @return void + */ + public function setOption($option, $value) + { + switch ($option) { + case self::OPTION_API: + case self::OPTION_TIMEOUT: + break; + default: + throw new Services_oEmbed_Exception( + 'Invalid option "' . $option . '"' + ); + } + + $func = '_set_' . $option; + if (method_exists($this, $func)) { + $this->options[$option] = $this->$func($value); + } else { + $this->options[$option] = $value; + } + } + + /** + * Set the API option + * + * @param string $value The API's URI + * + * @throws {@link Services_oEmbed_Exception} on invalid API URI + * @see Validate::uri() + * @return string + */ + protected function _set_oembed_api($value) + { + if (!Validate::uri($value)) { + throw new Services_oEmbed_Exception( + 'API URI provided is invalid' + ); + } + + return $value; + } + + /** + * Get the oEmbed response + * + * @param array $params Optional parameters for + * + * @throws {@link Services_oEmbed_Exception} on cURL errors + * @throws {@link Services_oEmbed_Exception} on HTTP errors + * @throws {@link Services_oEmbed_Exception} when result is not parsable + * @return object The oEmbed response as an object + */ + public function getObject(array $params = array()) + { + $params['url'] = $this->url->getURL(); + if (!isset($params['format'])) { + $params['format'] = 'json'; + } + + $sets = array(); + foreach ($params as $var => $val) { + $sets[] = $var . '=' . urlencode($val); + } + + $url = $this->options[self::OPTION_API] . '?' . implode('&', $sets); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->options[self::OPTION_TIMEOUT]); + $result = curl_exec($ch); + + if (curl_errno($ch)) { + throw new Services_oEmbed_Exception( + curl_error($ch), curl_errno($ch) + ); + } + + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if (substr($code, 0, 1) != '2') { + throw new Services_oEmbed_Exception('Non-200 code returned'); + } + + curl_close($ch); + + switch ($params['format']) { + case 'json': + $res = json_decode($result); + if (!is_object($res)) { + throw new Services_oEmbed_Exception( + 'Could not parse JSON response' + ); + } + break; + case 'xml': + libxml_use_internal_errors(true); + $res = simplexml_load_string($result); + if (!$res instanceof SimpleXMLElement) { + $errors = libxml_get_errors(); + $err = array_shift($errors); + libxml_clear_errors(); + libxml_use_internal_errors(false); + throw new Services_oEmbed_Exception( + $err->message, $error->code + ); + } + break; + } + + return Services_oEmbed_Object::factory($res); + } + + /** + * Discover an oEmbed API + * + * @param string $url The URL to attempt to discover oEmbed for + * + * @throws {@link Services_oEmbed_Exception} if the $url is invalid + * @return string The oEmbed API endpoint discovered + */ + protected function discover($url) + { + $body = $this->sendRequest($url); + + // Find all tags that have a valid oembed type set. We then + // extract the href attribute for each type. + $regexp = '#]*)type="' . + '(application/json|text/xml)\+oembed"([^>]*)>#i'; + + $m = $ret = array(); + if (!preg_match_all($regexp, $body, $m)) { + throw new Services_oEmbed_Exception_NoSupport( + 'No valid oEmbed links found on page' + ); + } + + foreach ($m[0] as $i => $link) { + $h = array(); + if (preg_match('/href="([^"]+)"/i', $link, $h)) { + $ret[$m[2][$i]] = $h[1]; + } + } + + return (isset($ret['json']) ? $ret['json'] : array_pop($ret)); + } + + /** + * Send a GET request to the provider + * + * @param mixed $url The URL to send the request to + * + * @throws {@link Services_oEmbed_Exception} on HTTP errors + * @return string The contents of the response + */ + private function sendRequest($url) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->options[self::OPTION_TIMEOUT]); + curl_setopt($ch, CURLOPT_USERAGENT, $this->options[self::OPTION_USER_AGENT]); + $result = curl_exec($ch); + if (curl_errno($ch)) { + throw new Services_oEmbed_Exception( + curl_error($ch), curl_errno($ch) + ); + } + + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if (substr($code, 0, 1) != '2') { + throw new Services_oEmbed_Exception('Non-200 code returned'); + } + + return $result; + } +} + +?> diff --git a/extlib/Services/oEmbed/Exception.php b/extlib/Services/oEmbed/Exception.php new file mode 100644 index 000000000..446ac2a70 --- /dev/null +++ b/extlib/Services/oEmbed/Exception.php @@ -0,0 +1,65 @@ + + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version SVN: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ + +require_once 'PEAR/Exception.php'; + +/** + * Base exception class for {@link Services_oEmbed} + * + * @category Services + * @package Services_oEmbed + * @author Joe Stump + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version Release: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ +class Services_oEmbed_Exception extends PEAR_Exception +{ + +} + +?> diff --git a/extlib/Services/oEmbed/Exception/NoSupport.php b/extlib/Services/oEmbed/Exception/NoSupport.php new file mode 100644 index 000000000..384c7191f --- /dev/null +++ b/extlib/Services/oEmbed/Exception/NoSupport.php @@ -0,0 +1,63 @@ + + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version SVN: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ + +/** + * Exception class when no oEmbed support is discovered + * + * @category Services + * @package Services_oEmbed + * @author Joe Stump + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version Release: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ +class Services_oEmbed_Exception_NoSupport extends Services_oEmbed_Exception +{ + +} + +?> diff --git a/extlib/Services/oEmbed/Object.php b/extlib/Services/oEmbed/Object.php new file mode 100644 index 000000000..9eedd7efb --- /dev/null +++ b/extlib/Services/oEmbed/Object.php @@ -0,0 +1,126 @@ + + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version SVN: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ + +require_once 'Services/oEmbed/Object/Exception.php'; + +/** + * Base class for consuming oEmbed objects + * + * @category Services + * @package Services_oEmbed + * @author Joe Stump + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version Release: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ +abstract class Services_oEmbed_Object +{ + + /** + * Valid oEmbed object types + * + * @var array $types Array of valid object types + * @see Services_oEmbed_Object::factory() + */ + static protected $types = array( + 'photo' => 'Photo', + 'video' => 'Video', + 'link' => 'Link', + 'rich' => 'Rich' + ); + + /** + * Create an oEmbed object from result + * + * @param object $object Raw object returned from API + * + * @throws {@link Services_oEmbed_Object_Exception} on object error + * @return object Instance of object driver + * @see Services_oEmbed_Object_Link, Services_oEmbed_Object_Photo + * @see Services_oEmbed_Object_Rich, Services_oEmbed_Object_Video + */ + static public function factory($object) + { + if (!isset($object->type)) { + throw new Services_oEmbed_Object_Exception( + 'Object has no type' + ); + } + + $type = (string)$object->type; + if (!isset(self::$types[$type])) { + throw new Services_oEmbed_Object_Exception( + 'Object type is unknown or invalid: ' . $type + ); + } + + $file = 'Services/oEmbed/Object/' . self::$types[$type] . '.php'; + include_once $file; + + $class = 'Services_oEmbed_Object_' . self::$types[$type]; + if (!class_exists($class)) { + throw new Services_oEmbed_Object_Exception( + 'Object class is invalid or not present' + ); + } + + $instance = new $class($object); + return $instance; + } + + /** + * Instantiation is not allowed + * + * @return void + */ + private function __construct() + { + + } +} + +?> diff --git a/extlib/Services/oEmbed/Object/Common.php b/extlib/Services/oEmbed/Object/Common.php new file mode 100644 index 000000000..f568ec89f --- /dev/null +++ b/extlib/Services/oEmbed/Object/Common.php @@ -0,0 +1,139 @@ + + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version SVN: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ + +/** + * Base class for oEmbed objects + * + * @category Services + * @package Services_oEmbed + * @author Joe Stump + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version Release: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ +abstract class Services_oEmbed_Object_Common +{ + /** + * Raw object returned from API + * + * @var object $object The raw object from the API + */ + protected $object = null; + + /** + * Required fields per the specification + * + * @var array $required Array of required fields + * @link http://oembed.com + */ + protected $required = array(); + + /** + * Constructor + * + * @param object $object Raw object returned from the API + * + * @throws {@link Services_oEmbed_Object_Exception} on missing fields + * @return void + */ + public function __construct($object) + { + $this->object = $object; + + $this->required[] = 'version'; + foreach ($this->required as $field) { + if (!isset($this->$field)) { + throw new Services_oEmbed_Object_Exception( + 'Object is missing required ' . $field . ' attribute' + ); + } + } + } + + /** + * Get object variable + * + * @param string $var Variable to get + * + * @see Services_oEmbed_Object_Common::$object + * @return mixed Attribute's value or null if it's not set/exists + */ + public function __get($var) + { + if (property_exists($this->object, $var)) { + return $this->object->$var; + } + + return null; + } + + /** + * Is variable set? + * + * @param string $var Variable name to check + * + * @return boolean True if set, false if not + * @see Services_oEmbed_Object_Common::$object + */ + public function __isset($var) + { + if (property_exists($this->object, $var)) { + return (isset($this->object->$var)); + } + + return false; + } + + /** + * Require a sane __toString for all objects + * + * @return string + */ + abstract public function __toString(); +} + +?> diff --git a/extlib/Services/oEmbed/Object/Exception.php b/extlib/Services/oEmbed/Object/Exception.php new file mode 100644 index 000000000..6025ffd49 --- /dev/null +++ b/extlib/Services/oEmbed/Object/Exception.php @@ -0,0 +1,65 @@ + + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version SVN: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ + +require_once 'Services/oEmbed/Exception.php'; + +/** + * Exception for {@link Services_oEmbed_Object} + * + * @category Services + * @package Services_oEmbed + * @author Joe Stump + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version Release: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ +class Services_oEmbed_Object_Exception extends Services_oEmbed_Exception +{ + +} + +?> diff --git a/extlib/Services/oEmbed/Object/Link.php b/extlib/Services/oEmbed/Object/Link.php new file mode 100644 index 000000000..9b627a89a --- /dev/null +++ b/extlib/Services/oEmbed/Object/Link.php @@ -0,0 +1,73 @@ + + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version SVN: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ + +require_once 'Services/oEmbed/Object/Common.php'; + +/** + * Link object for {@link Services_oEmbed} + * + * @category Services + * @package Services_oEmbed + * @author Joe Stump + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version Release: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ +class Services_oEmbed_Object_Link extends Services_oEmbed_Object_Common +{ + /** + * Output a sane link + * + * @return string An HTML link of the object + */ + public function __toString() + { + return '' . $this->title . ''; + } +} + +?> diff --git a/extlib/Services/oEmbed/Object/Photo.php b/extlib/Services/oEmbed/Object/Photo.php new file mode 100644 index 000000000..5fbf4292f --- /dev/null +++ b/extlib/Services/oEmbed/Object/Photo.php @@ -0,0 +1,89 @@ + + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version SVN: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ + +require_once 'Services/oEmbed/Object/Common.php'; + +/** + * Photo object for {@link Services_oEmbed} + * + * @category Services + * @package Services_oEmbed + * @author Joe Stump + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version Release: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ +class Services_oEmbed_Object_Photo extends Services_oEmbed_Object_Common +{ + /** + * Required fields for photo objects + * + * @var array $required Required fields + */ + protected $required = array( + 'url', 'width', 'height' + ); + + /** + * Output a valid HTML tag for image + * + * @return string HTML tag for Photo + */ + public function __toString() + { + $img = 'title)) { + $img .= ' alt="' . $this->title . '"'; + } + + return $img . ' />'; + } +} + +?> diff --git a/extlib/Services/oEmbed/Object/Rich.php b/extlib/Services/oEmbed/Object/Rich.php new file mode 100644 index 000000000..dbf6933ac --- /dev/null +++ b/extlib/Services/oEmbed/Object/Rich.php @@ -0,0 +1,82 @@ + + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version SVN: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ + +require_once 'Services/oEmbed/Object/Common.php'; + +/** + * Photo object for {@link Services_oEmbed} + * + * @category Services + * @package Services_oEmbed + * @author Joe Stump + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version Release: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ +class Services_oEmbed_Object_Rich extends Services_oEmbed_Object_Common +{ + /** + * Required fields for rich objects + * + * @var array $required Required fields + */ + protected $required = array( + 'html', 'width', 'height' + ); + + /** + * Output a the HTML tag for rich object + * + * @return string HTML for rich object + */ + public function __toString() + { + return $this->html; + } +} + +?> diff --git a/extlib/Services/oEmbed/Object/Video.php b/extlib/Services/oEmbed/Object/Video.php new file mode 100644 index 000000000..746108115 --- /dev/null +++ b/extlib/Services/oEmbed/Object/Video.php @@ -0,0 +1,82 @@ + + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version SVN: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ + +require_once 'Services/oEmbed/Object/Common.php'; + +/** + * Photo object for {@link Services_oEmbed} + * + * @category Services + * @package Services_oEmbed + * @author Joe Stump + * @copyright 2008 Digg.com, Inc. + * @license http://tinyurl.com/42zef New BSD License + * @version Release: @version@ + * @link http://code.google.com/p/digg + * @link http://oembed.com + */ +class Services_oEmbed_Object_Video extends Services_oEmbed_Object_Common +{ + /** + * Required fields for video objects + * + * @var array $required Required fields + */ + protected $required = array( + 'html', 'width', 'height' + ); + + /** + * Output a valid embed tag for video + * + * @return string HTML for video + */ + public function __toString() + { + return $this->html; + } +} + +?> diff --git a/extlib/Validate.php b/extlib/Validate.php index 4c05506b3..3d8bc23f2 100644 --- a/extlib/Validate.php +++ b/extlib/Validate.php @@ -1,1051 +1,1116 @@ - | -// | Pierre-Alain Joye | -// | Amir Mohammad Saied | -// +----------------------------------------------------------------------+ -// -/** - * Validation class - * - * Package to validate various datas. It includes : - * - numbers (min/max, decimal or not) - * - email (syntax, domain check) - * - string (predifined type alpha upper and/or lowercase, numeric,...) - * - date (min, max, rfc822 compliant) - * - uri (RFC2396) - * - possibility valid multiple data with a single method call (::multiple) - * - * @category Validate - * @package Validate - * @author Tomas V.V.Cox - * @author Pierre-Alain Joye - * @author Amir Mohammad Saied - * @copyright 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox,Amir Mohammad Saied - * @license http://www.opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Validate.php,v 1.123 2007/12/12 16:45:51 davidc Exp $ - * @link http://pear.php.net/package/Validate - */ - -/** - * Methods for common data validations - */ -define('VALIDATE_NUM', '0-9'); -define('VALIDATE_SPACE', '\s'); -define('VALIDATE_ALPHA_LOWER', 'a-z'); -define('VALIDATE_ALPHA_UPPER', 'A-Z'); -define('VALIDATE_ALPHA', VALIDATE_ALPHA_LOWER . VALIDATE_ALPHA_UPPER); -define('VALIDATE_EALPHA_LOWER', VALIDATE_ALPHA_LOWER . 'áéíóúýàèìòùäëïöüÿâêîôûãñõ¨åæç½ðøþß'); -define('VALIDATE_EALPHA_UPPER', VALIDATE_ALPHA_UPPER . 'ÁÉÍÓÚÝÀÈÌÒÙÄËÏÖܾÂÊÎÔÛÃÑÕ¦ÅÆǼÐØÞ'); -define('VALIDATE_EALPHA', VALIDATE_EALPHA_LOWER . VALIDATE_EALPHA_UPPER); -define('VALIDATE_PUNCTUATION', VALIDATE_SPACE . '\.,;\:&"\'\?\!\(\)'); -define('VALIDATE_NAME', VALIDATE_EALPHA . VALIDATE_SPACE . "'" . "-"); -define('VALIDATE_STREET', VALIDATE_NUM . VALIDATE_NAME . "/\\ºª\."); - -define('VALIDATE_ITLD_EMAILS', 1); -define('VALIDATE_GTLD_EMAILS', 2); -define('VALIDATE_CCTLD_EMAILS', 4); -define('VALIDATE_ALL_EMAILS', 8); - -/** - * Validation class - * - * Package to validate various datas. It includes : - * - numbers (min/max, decimal or not) - * - email (syntax, domain check) - * - string (predifined type alpha upper and/or lowercase, numeric,...) - * - date (min, max) - * - uri (RFC2396) - * - possibility valid multiple data with a single method call (::multiple) - * - * @category Validate - * @package Validate - * @author Tomas V.V.Cox - * @author Pierre-Alain Joye - * @author Amir Mohammad Saied - * @copyright 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox,Amir Mohammad Saied - * @license http://www.opensource.org/licenses/bsd-license.php New BSD License - * @version Release: @package_version@ - * @link http://pear.php.net/package/Validate - */ -class Validate -{ - /** - * International Top-Level Domain - * - * This is an array of the known international - * top-level domain names. - * - * @access protected - * @var array $_iTld (International top-level domains) - */ - var $_itld = array( - 'arpa', - 'root', - ); - - /** - * Generic top-level domain - * - * This is an array of the official - * generic top-level domains. - * - * @access protected - * @var array $_gTld (Generic top-level domains) - */ - var $_gtld = array( - 'aero', - 'biz', - 'cat', - 'com', - 'coop', - 'edu', - 'gov', - 'info', - 'int', - 'jobs', - 'mil', - 'mobi', - 'museum', - 'name', - 'net', - 'org', - 'pro', - 'travel', - 'asia', - 'post', - 'tel', - 'geo', - ); - - /** - * Country code top-level domains - * - * This is an array of the official country - * codes top-level domains - * - * @access protected - * @var array $_ccTld (Country Code Top-Level Domain) - */ - var $_cctld = array( - 'ac', - 'ad','ae','af','ag', - 'ai','al','am','an', - 'ao','aq','ar','as', - 'at','au','aw','ax', - 'az','ba','bb','bd', - 'be','bf','bg','bh', - 'bi','bj','bm','bn', - 'bo','br','bs','bt', - 'bu','bv','bw','by', - 'bz','ca','cc','cd', - 'cf','cg','ch','ci', - 'ck','cl','cm','cn', - 'co','cr','cs','cu', - 'cv','cx','cy','cz', - 'de','dj','dk','dm', - 'do','dz','ec','ee', - 'eg','eh','er','es', - 'et','eu','fi','fj', - 'fk','fm','fo','fr', - 'ga','gb','gd','ge', - 'gf','gg','gh','gi', - 'gl','gm','gn','gp', - 'gq','gr','gs','gt', - 'gu','gw','gy','hk', - 'hm','hn','hr','ht', - 'hu','id','ie','il', - 'im','in','io','iq', - 'ir','is','it','je', - 'jm','jo','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','mk', - 'ml','mm','mn','mo', - 'mp','mq','mr','ms', - 'mt','mu','mv','mw', - 'mx','my','mz','na', - 'nc','ne','nf','ng', - 'ni','nl','no','np', - 'nr','nu','nz','om', - 'pa','pe','pf','pg', - 'ph','pk','pl','pm', - 'pn','pr','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','tf','tg', - 'th','tj','tk','tl', - 'tm','tn','to','tp', - 'tr','tt','tv','tw', - 'tz','ua','ug','uk', - 'us','uy','uz','va', - 'vc','ve','vg','vi', - 'vn','vu','wf','ws', - 'ye','yt','yu','za', - 'zm','zw', - ); - - - /** - * Validate a number - * - * @param string $number Number to validate - * @param array $options array where: - * 'decimal' is the decimal char or false when decimal not allowed - * i.e. ',.' to allow both ',' and '.' - * 'dec_prec' Number of allowed decimals - * 'min' minimum value - * 'max' maximum value - * - * @return boolean true if valid number, false if not - * - * @access public - */ - function number($number, $options = array()) - { - $decimal = $dec_prec = $min = $max = null; - if (is_array($options)) { - extract($options); - } - - $dec_prec = $dec_prec ? "{1,$dec_prec}" : '+'; - $dec_regex = $decimal ? "[$decimal][0-9]$dec_prec" : ''; - - if (!preg_match("|^[-+]?\s*[0-9]+($dec_regex)?\$|", $number)) { - return false; - } - - if ($decimal != '.') { - $number = strtr($number, $decimal, '.'); - } - - $number = (float)str_replace(' ', '', $number); - if ($min !== null && $min > $number) { - return false; - } - - if ($max !== null && $max < $number) { - return false; - } - return true; - } - - /** - * Converting a string to UTF-7 (RFC 2152) - * - * @param $string string to be converted - * - * @return string converted string - * - * @access private - */ - function __stringToUtf7($string) { - $return = ''; - $utf7 = array( - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', - 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', - 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', - 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', - 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', - '3', '4', '5', '6', '7', '8', '9', '+', ',' - ); - - $state = 0; - if (!empty($string)) { - $i = 0; - while ($i <= strlen($string)) { - $char = substr($string, $i, 1); - if ($state == 0) { - if ((ord($char) >= 0x7F) || (ord($char) <= 0x1F)) { - if ($char) { - $return .= '&'; - } - $state = 1; - } elseif ($char == '&') { - $return .= '&-'; - } else { - $return .= $char; - } - } elseif (($i == strlen($string) || - !((ord($char) >= 0x7F)) || (ord($char) <= 0x1F))) { - if ($state != 1) { - if (ord($char) > 64) { - $return .= ''; - } else { - $return .= $utf7[ord($char)]; - } - } - $return .= '-'; - $state = 0; - } else { - switch($state) { - case 1: - $return .= $utf7[ord($char) >> 2]; - $residue = (ord($char) & 0x03) << 4; - $state = 2; - break; - case 2: - $return .= $utf7[$residue | (ord($char) >> 4)]; - $residue = (ord($char) & 0x0F) << 2; - $state = 3; - break; - case 3: - $return .= $utf7[$residue | (ord($char) >> 6)]; - $return .= $utf7[ord($char) & 0x3F]; - $state = 1; - break; - } - } - $i++; - } - return $return; - } - return ''; - } - - /** - * Validate an email according to full RFC822 (inclusive human readable part) - * - * @param string $email email to validate, - * will return the address for optional dns validation - * @param array $options email() options - * - * @return boolean true if valid email, false if not - * - * @access private - */ - function __emailRFC822(&$email, &$options) - { - if (Validate::__stringToUtf7($email) != $email) { - return false; - } - static $address = null; - static $uncomment = null; - if (!$address) { - // atom = 1* - $atom = '[^][()<>@,;:\\".\s\000-\037\177-\377]+\s*'; - // qtext = , ; => may be folded - // "\" & CR, and including linear-white-space> - $qtext = '[^"\\\\\r]'; - // quoted-pair = "\" CHAR ; may quote any char - $quoted_pair = '\\\\.'; - // quoted-string = <"> *(qtext/quoted-pair) <">; Regular qtext or - // ; quoted chars. - $quoted_string = '"(?:' . $qtext . '|' . $quoted_pair . ')*"\s*'; - // word = atom / quoted-string - $word = '(?:' . $atom . '|' . $quoted_string . ')'; - // local-part = word *("." word) ; uninterpreted - // ; case-preserved - $local_part = $word . '(?:\.\s*' . $word . ')*'; - // dtext = may be folded - // "]", "\" & CR, & including linear-white-space> - $dtext = '[^][\\\\\r]'; - // domain-literal = "[" *(dtext / quoted-pair) "]" - $domain_literal = '\[(?:' . $dtext . '|' . $quoted_pair . ')*\]\s*'; - // sub-domain = domain-ref / domain-literal - // domain-ref = atom ; symbolic reference - $sub_domain = '(?:' . $atom . '|' . $domain_literal . ')'; - // domain = sub-domain *("." sub-domain) - $domain = $sub_domain . '(?:\.\s*' . $sub_domain . ')*'; - // addr-spec = local-part "@" domain ; global address - $addr_spec = $local_part . '@\s*' . $domain; - // route = 1#("@" domain) ":" ; path-relative - $route = '@' . $domain . '(?:,@\s*' . $domain . ')*:\s*'; - // route-addr = "<" [route] addr-spec ">" - $route_addr = '<\s*(?:' . $route . ')?' . $addr_spec . '>\s*'; - // phrase = 1*word ; Sequence of words - $phrase = $word . '+'; - // mailbox = addr-spec ; simple address - // / phrase route-addr ; name & addr-spec - $mailbox = '(?:' . $addr_spec . '|' . $phrase . $route_addr . ')'; - // group = phrase ":" [#mailbox] ";" - $group = $phrase . ':\s*(?:' . $mailbox . '(?:,\s*' . $mailbox . ')*)?;\s*'; - // address = mailbox ; one addressee - // / group ; named list - $address = '/^\s*(?:' . $mailbox . '|' . $group . ')$/'; - $uncomment = - '/((?:(?:\\\\"|[^("])*(?:' . $quoted_string . - ')?)*)((?{$tmpVar}; - } - - $e = $self->executeFullEmailValidation($email, $toValidate); - - return $e; - } - // {{{ protected function executeFullEmailValidation - /** - * Execute the validation - * - * This function will execute the full email vs tld - * validation using an array of tlds passed to it. - * - * @access public - * @param string $email The email to validate. - * @param array $arrayOfTLDs The array of the TLDs to validate - * @return true or false (Depending on if it validates or if it does not) - */ - function executeFullEmailValidation($email, $arrayOfTLDs) - { - $emailEnding = explode('.', $email); - $emailEnding = $emailEnding[count($emailEnding)-1]; - - foreach ($arrayOfTLDs as $validator => $keys) { - if (in_array($emailEnding, $keys)) { - return true; - } - } - return false; - } - // }}} - - /** - * Validate an email - * - * @param string $email email to validate - * @param mixed boolean (BC) $check_domain Check or not if the domain exists - * array $options associative array of options - * 'check_domain' boolean Check or not if the domain exists - * 'use_rfc822' boolean Apply the full RFC822 grammar - * - * @return boolean true if valid email, false if not - * - * @access public - */ - function email($email, $options = null) - { - $check_domain = false; - $use_rfc822 = false; - if (is_bool($options)) { - $check_domain = $options; - } elseif (is_array($options)) { - extract($options); - } - - /** - * @todo Fix bug here.. even if it passes this, it won't be passing - * The regular expression below - */ - if (isset($fullTLDValidation)) { - $valid = Validate::_fullTLDValidation($email, $fullTLDValidation); - - if (!$valid) { - return false; - } - } - - // the base regexp for address - $regex = '&^(?: # recipient: - ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| #1 quoted name - ([-\w!\#\$%\&\'*+~/^`|{}]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}]+)*)) #2 OR dot-atom - @(((\[)? #3 domain, 4 as IPv4, 5 optionally bracketed - (?:(?:(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:[0-1]?[0-9]?[0-9]))\.){3} - (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:[0-1]?[0-9]?[0-9]))))(?(5)\])| - ((?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*[a-z0-9](?:[-a-z0-9]*[a-z0-9])?) #6 domain as hostname - \.((?:([^- ])[-a-z]*[-a-z]))) #7 TLD - $&xi'; - - if ($use_rfc822? Validate::__emailRFC822($email, $options) : - preg_match($regex, $email)) { - if ($check_domain && function_exists('checkdnsrr')) { - list (, $domain) = explode('@', $email); - if (checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A')) { - return true; - } - return false; - } - return true; - } - return false; - } - - /** - * Validate a string using the given format 'format' - * - * @param string $string String to validate - * @param array $options Options array where: - * 'format' is the format of the string - * Ex: VALIDATE_NUM . VALIDATE_ALPHA (see constants) - * 'min_length' minimum length - * 'max_length' maximum length - * - * @return boolean true if valid string, false if not - * - * @access public - */ - function string($string, $options) - { - $format = null; - $min_length = $max_length = 0; - if (is_array($options)) { - extract($options); - } - if ($format && !preg_match("|^[$format]*\$|s", $string)) { - return false; - } - if ($min_length && strlen($string) < $min_length) { - return false; - } - if ($max_length && strlen($string) > $max_length) { - return false; - } - return true; - } - - /** - * Validate an URI (RFC2396) - * This function will validate 'foobarstring' by default, to get it to validate - * only http, https, ftp and such you have to pass it in the allowed_schemes - * option, like this: - * - * $options = array('allowed_schemes' => array('http', 'https', 'ftp')) - * var_dump(Validate::uri('http://www.example.org', $options)); - * - * - * NOTE 1: The rfc2396 normally allows middle '-' in the top domain - * e.g. http://example.co-m should be valid - * However, as '-' is not used in any known TLD, it is invalid - * NOTE 2: As double shlashes // are allowed in the path part, only full URIs - * including an authority can be valid, no relative URIs - * the // are mandatory (optionally preceeded by the 'sheme:' ) - * NOTE 3: the full complience to rfc2396 is not achieved by default - * the characters ';/?:@$,' will not be accepted in the query part - * if not urlencoded, refer to the option "strict'" - * - * @param string $url URI to validate - * @param array $options Options used by the validation method. - * key => type - * 'domain_check' => boolean - * Whether to check the DNS entry or not - * 'allowed_schemes' => array, list of protocols - * List of allowed schemes ('http', - * 'ssh+svn', 'mms') - * 'strict' => string the refused chars - * in query and fragment parts - * default: ';/?:@$,' - * empty: accept all rfc2396 foreseen chars - * - * @return boolean true if valid uri, false if not - * - * @access public - */ - function uri($url, $options = null) - { - $strict = ';/?:@$,'; - $domain_check = false; - $allowed_schemes = null; - if (is_array($options)) { - extract($options); - } - if (preg_match( - '&^(?:([a-z][-+.a-z0-9]*):)? # 1. scheme - (?:// # authority start - (?:((?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'();:\&=+$,])*)@)? # 2. authority-userinfo - (?:((?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*[a-z](?:[a-z0-9]+)?\.?) # 3. authority-hostname OR - |([0-9]{1,3}(?:\.[0-9]{1,3}){3})) # 4. authority-ipv4 - (?::([0-9]*))?) # 5. authority-port - ((?:/(?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'():@\&=+$,;])*)*/?)? # 6. path - (?:\?([^#]*))? # 7. query - (?:\#((?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'();/?:@\&=+$,])*))? # 8. fragment - $&xi', $url, $matches)) { - $scheme = isset($matches[1]) ? $matches[1] : ''; - $authority = isset($matches[3]) ? $matches[3] : '' ; - if (is_array($allowed_schemes) && - !in_array($scheme,$allowed_schemes) - ) { - return false; - } - if (!empty($matches[4])) { - $parts = explode('.', $matches[4]); - foreach ($parts as $part) { - if ($part > 255) { - return false; - } - } - } elseif ($domain_check && function_exists('checkdnsrr')) { - if (!checkdnsrr($authority, 'A')) { - return false; - } - } - if ($strict) { - $strict = '#[' . preg_quote($strict, '#') . ']#'; - if ((!empty($matches[7]) && preg_match($strict, $matches[7])) - || (!empty($matches[8]) && preg_match($strict, $matches[8]))) { - return false; - } - } - return true; - } - return false; - } - - /** - * Validate date and times. Note that this method need the Date_Calc class - * - * @param string $date Date to validate - * @param array $options array options where : - * 'format' The format of the date (%d-%m-%Y) - * or rfc822_compliant - * 'min' The date has to be greater - * than this array($day, $month, $year) - * or PEAR::Date object - * 'max' The date has to be smaller than - * this array($day, $month, $year) - * or PEAR::Date object - * - * @return boolean true if valid date/time, false if not - * - * @access public - */ - function date($date, $options) - { - $max = $min = false; - $format = ''; - if (is_array($options)) { - extract($options); - } - - if (strtolower($format) == 'rfc822_compliant') { - $preg = '&^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),) \s+ - (?:(\d{2})?) \s+ - (?:(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)?) \s+ - (?:(\d{2}(\d{2})?)?) \s+ - (?:(\d{2}?)):(?:(\d{2}?))(:(?:(\d{2}?)))? \s+ - (?:[+-]\d{4}|UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[A-IK-Za-ik-z])$&xi'; - - if (!preg_match($preg, $date, $matches)) { - return false; - } - - $year = (int)$matches[4]; - $months = array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'); - $month = array_keys($months, $matches[3]); - $month = (int)$month[0]+1; - $day = (int)$matches[2]; - $weekday= $matches[1]; - $hour = (int)$matches[6]; - $minute = (int)$matches[7]; - isset($matches[9]) ? $second = (int)$matches[9] : $second = 0; - - if ((strlen($year) != 4) || - ($day > 31 || $day < 1)|| - ($hour > 23) || - ($minute > 59) || - ($second > 59)) { - return false; - } - } else { - $date_len = strlen($format); - for ($i = 0; $i < $date_len; $i++) { - $c = $format{$i}; - if ($c == '%') { - $next = $format{$i + 1}; - switch ($next) { - case 'j': - case 'd': - if ($next == 'j') { - $day = (int)Validate::_substr($date, 1, 2); - } else { - $day = (int)Validate::_substr($date, 0, 2); - } - if ($day < 1 || $day > 31) { - return false; - } - break; - case 'm': - case 'n': - if ($next == 'm') { - $month = (int)Validate::_substr($date, 0, 2); - } else { - $month = (int)Validate::_substr($date, 1, 2); - } - if ($month < 1 || $month > 12) { - return false; - } - break; - case 'Y': - case 'y': - if ($next == 'Y') { - $year = Validate::_substr($date, 4); - $year = (int)$year?$year:''; - } else { - $year = (int)(substr(date('Y'), 0, 2) . - Validate::_substr($date, 2)); - } - if (strlen($year) != 4 || $year < 0 || $year > 9999) { - return false; - } - break; - case 'g': - case 'h': - if ($next == 'g') { - $hour = Validate::_substr($date, 1, 2); - } else { - $hour = Validate::_substr($date, 2); - } - if (!preg_match('/^\d+$/', $hour) || $hour < 0 || $hour > 12) { - return false; - } - break; - case 'G': - case 'H': - if ($next == 'G') { - $hour = Validate::_substr($date, 1, 2); - } else { - $hour = Validate::_substr($date, 2); - } - if (!preg_match('/^\d+$/', $hour) || $hour < 0 || $hour > 24) { - return false; - } - break; - case 's': - case 'i': - $t = Validate::_substr($date, 2); - if (!preg_match('/^\d+$/', $t) || $t < 0 || $t > 59) { - return false; - } - break; - default: - trigger_error("Not supported char `$next' after % in offset " . ($i+2), E_USER_WARNING); - } - $i++; - } else { - //literal - if (Validate::_substr($date, 1) != $c) { - return false; - } - } - } - } - // there is remaing data, we don't want it - if (strlen($date) && (strtolower($format) != 'rfc822_compliant')) { - return false; - } - - if (isset($day) && isset($month) && isset($year)) { - if (!checkdate($month, $day, $year)) { - return false; - } - - if (strtolower($format) == 'rfc822_compliant') { - if ($weekday != date("D", mktime(0, 0, 0, $month, $day, $year))) { - return false; - } - } - - if ($min) { - include_once 'Date/Calc.php'; - if (is_a($min, 'Date') && - (Date_Calc::compareDates($day, $month, $year, - $min->getDay(), $min->getMonth(), $min->getYear()) < 0)) - { - return false; - } elseif (is_array($min) && - (Date_Calc::compareDates($day, $month, $year, - $min[0], $min[1], $min[2]) < 0)) - { - return false; - } - } - - if ($max) { - include_once 'Date/Calc.php'; - if (is_a($max, 'Date') && - (Date_Calc::compareDates($day, $month, $year, - $max->getDay(), $max->getMonth(), $max->getYear()) > 0)) - { - return false; - } elseif (is_array($max) && - (Date_Calc::compareDates($day, $month, $year, - $max[0], $max[1], $max[2]) > 0)) - { - return false; - } - } - } - - return true; - } - - function _substr(&$date, $num, $opt = false) - { - if ($opt && strlen($date) >= $opt && preg_match('/^[0-9]{'.$opt.'}/', $date, $m)) { - $ret = $m[0]; - } else { - $ret = substr($date, 0, $num); - } - $date = substr($date, strlen($ret)); - return $ret; - } - - function _modf($val, $div) { - if (function_exists('bcmod')) { - return bcmod($val, $div); - } elseif (function_exists('fmod')) { - return fmod($val, $div); - } - $r = $val / $div; - $i = intval($r); - return intval($val - $i * $div + .1); - } - - /** - * Calculates sum of product of number digits with weights - * - * @param string $number number string - * @param array $weights reference to array of weights - * - * @returns int returns product of number digits with weights - * - * @access protected - */ - function _multWeights($number, &$weights) { - if (!is_array($weights)) { - return -1; - } - $sum = 0; - - $count = min(count($weights), strlen($number)); - if ($count == 0) { // empty string or weights array - return -1; - } - for ($i = 0; $i < $count; ++$i) { - $sum += intval(substr($number, $i, 1)) * $weights[$i]; - } - - return $sum; - } - - /** - * Calculates control digit for a given number - * - * @param string $number number string - * @param array $weights reference to array of weights - * @param int $modulo (optionsl) number - * @param int $subtract (optional) number - * @param bool $allow_high (optional) true if function can return number higher than 10 - * - * @returns int -1 calculated control number is returned - * - * @access protected - */ - function _getControlNumber($number, &$weights, $modulo = 10, $subtract = 0, $allow_high = false) { - // calc sum - $sum = Validate::_multWeights($number, $weights); - if ($sum == -1) { - return -1; - } - $mod = Validate::_modf($sum, $modulo); // calculate control digit - - if ($subtract > $mod && $mod > 0) { - $mod = $subtract - $mod; - } - if ($allow_high === false) { - $mod %= 10; // change 10 to zero - } - return $mod; - } - - /** - * Validates a number - * - * @param string $number number to validate - * @param array $weights reference to array of weights - * @param int $modulo (optionsl) number - * @param int $subtract (optional) numbier - * - * @returns bool true if valid, false if not - * - * @access protected - */ - function _checkControlNumber($number, &$weights, $modulo = 10, $subtract = 0) { - if (strlen($number) < count($weights)) { - return false; - } - $target_digit = substr($number, count($weights), 1); - $control_digit = Validate::_getControlNumber($number, $weights, $modulo, $subtract, $modulo > 10); - - if ($control_digit == -1) { - return false; - } - if ($target_digit === 'X' && $control_digit == 10) { - return true; - } - if ($control_digit != $target_digit) { - return false; - } - return true; - } - - /** - * Bulk data validation for data introduced in the form of an - * assoc array in the form $var_name => $value. - * Can be used on any of Validate subpackages - * - * @param array $data Ex: array('name' => 'toto', 'email' => 'toto@thing.info'); - * @param array $val_type Contains the validation type and all parameters used in. - * 'val_type' is not optional - * others validations properties must have the same name as the function - * parameters. - * Ex: array('toto'=>array('type'=>'string','format'='toto@thing.info','min_length'=>5)); - * @param boolean $remove if set, the elements not listed in data will be removed - * - * @return array value name => true|false the value name comes from the data key - * - * @access public - */ - function multiple(&$data, &$val_type, $remove = false) - { - $keys = array_keys($data); - $valid = array(); - foreach ($keys as $var_name) { - if (!isset($val_type[$var_name])) { - if ($remove) { - unset($data[$var_name]); - } - continue; - } - $opt = $val_type[$var_name]; - $methods = get_class_methods('Validate'); - $val2check = $data[$var_name]; - // core validation method - if (in_array(strtolower($opt['type']), $methods)) { - //$opt[$opt['type']] = $data[$var_name]; - $method = $opt['type']; - unset($opt['type']); - - if (sizeof($opt) == 1 && is_array(reset($opt))) { - $opt = array_pop($opt); - } - $valid[$var_name] = call_user_func(array('Validate', $method), $val2check, $opt); - - /** - * external validation method in the form: - * "" - * Ex: us_ssn will include class Validate/US.php and call method ssn() - */ - } elseif (strpos($opt['type'], '_') !== false) { - $validateType = explode('_', $opt['type']); - $method = array_pop($validateType); - $class = implode('_', $validateType); - $classPath = str_replace('_', DIRECTORY_SEPARATOR, $class); - $class = 'Validate_' . $class; - if (!@include_once "Validate/$classPath.php") { - trigger_error("$class isn't installed or you may have some permissoin issues", E_USER_ERROR); - } - - $ce = substr(phpversion(), 0, 1) > 4 ? class_exists($class, false) : class_exists($class); - if (!$ce || - !in_array($method, get_class_methods($class))) - { - trigger_error("Invalid validation type $class::$method", E_USER_WARNING); - continue; - } - unset($opt['type']); - if (sizeof($opt) == 1) { - $opt = array_pop($opt); - } - $valid[$var_name] = call_user_func(array($class, $method), $data[$var_name], $opt); - } else { - trigger_error("Invalid validation type {$opt['type']}", E_USER_WARNING); - } - } - return $valid; - } -} - + + * Pierre-Alain Joye + * Amir Mohammad Saied + * + * + * Package to validate various datas. It includes : + * - numbers (min/max, decimal or not) + * - email (syntax, domain check) + * - string (predifined type alpha upper and/or lowercase, numeric,...) + * - date (min, max, rfc822 compliant) + * - uri (RFC2396) + * - possibility valid multiple data with a single method call (::multiple) + * + * @category Validate + * @package Validate + * @author Tomas V.V.Cox + * @author Pierre-Alain Joye + * @author Amir Mohammad Saied + * @copyright 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox,Amir Mohammad Saied + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Validate.php,v 1.134 2009/01/28 12:27:33 davidc Exp $ + * @link http://pear.php.net/package/Validate + */ + +/** + * Methods for common data validations + */ +define('VALIDATE_NUM', '0-9'); +define('VALIDATE_SPACE', '\s'); +define('VALIDATE_ALPHA_LOWER', 'a-z'); +define('VALIDATE_ALPHA_UPPER', 'A-Z'); +define('VALIDATE_ALPHA', VALIDATE_ALPHA_LOWER . VALIDATE_ALPHA_UPPER); +define('VALIDATE_EALPHA_LOWER', VALIDATE_ALPHA_LOWER . 'áéíóúýàèìòùäëïöüÿâêîôûãñõ¨åæç½ðøþß'); +define('VALIDATE_EALPHA_UPPER', VALIDATE_ALPHA_UPPER . 'ÁÉÍÓÚÝÀÈÌÒÙÄËÏÖܾÂÊÎÔÛÃÑÕ¦ÅÆǼÐØÞ'); +define('VALIDATE_EALPHA', VALIDATE_EALPHA_LOWER . VALIDATE_EALPHA_UPPER); +define('VALIDATE_PUNCTUATION', VALIDATE_SPACE . '\.,;\:&"\'\?\!\(\)'); +define('VALIDATE_NAME', VALIDATE_EALPHA . VALIDATE_SPACE . "'" . "-"); +define('VALIDATE_STREET', VALIDATE_NUM . VALIDATE_NAME . "/\\ºª\."); + +define('VALIDATE_ITLD_EMAILS', 1); +define('VALIDATE_GTLD_EMAILS', 2); +define('VALIDATE_CCTLD_EMAILS', 4); +define('VALIDATE_ALL_EMAILS', 8); + +/** + * Validation class + * + * Package to validate various datas. It includes : + * - numbers (min/max, decimal or not) + * - email (syntax, domain check) + * - string (predifined type alpha upper and/or lowercase, numeric,...) + * - date (min, max) + * - uri (RFC2396) + * - possibility valid multiple data with a single method call (::multiple) + * + * @category Validate + * @package Validate + * @author Tomas V.V.Cox + * @author Pierre-Alain Joye + * @author Amir Mohammad Saied + * @copyright 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox,Amir Mohammad Saied + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Validate + */ +class Validate +{ + /** + * International Top-Level Domain + * + * This is an array of the known international + * top-level domain names. + * + * @access protected + * @var array $_iTld (International top-level domains) + */ + var $_itld = array( + 'arpa', + 'root', + ); + + /** + * Generic top-level domain + * + * This is an array of the official + * generic top-level domains. + * + * @access protected + * @var array $_gTld (Generic top-level domains) + */ + var $_gtld = array( + 'aero', + 'biz', + 'cat', + 'com', + 'coop', + 'edu', + 'gov', + 'info', + 'int', + 'jobs', + 'mil', + 'mobi', + 'museum', + 'name', + 'net', + 'org', + 'pro', + 'travel', + 'asia', + 'post', + 'tel', + 'geo', + ); + + /** + * Country code top-level domains + * + * This is an array of the official country + * codes top-level domains + * + * @access protected + * @var array $_ccTld (Country Code Top-Level Domain) + */ + var $_cctld = array( + 'ac', + 'ad','ae','af','ag', + 'ai','al','am','an', + 'ao','aq','ar','as', + 'at','au','aw','ax', + 'az','ba','bb','bd', + 'be','bf','bg','bh', + 'bi','bj','bm','bn', + 'bo','br','bs','bt', + 'bu','bv','bw','by', + 'bz','ca','cc','cd', + 'cf','cg','ch','ci', + 'ck','cl','cm','cn', + 'co','cr','cs','cu', + 'cv','cx','cy','cz', + 'de','dj','dk','dm', + 'do','dz','ec','ee', + 'eg','eh','er','es', + 'et','eu','fi','fj', + 'fk','fm','fo','fr', + 'ga','gb','gd','ge', + 'gf','gg','gh','gi', + 'gl','gm','gn','gp', + 'gq','gr','gs','gt', + 'gu','gw','gy','hk', + 'hm','hn','hr','ht', + 'hu','id','ie','il', + 'im','in','io','iq', + 'ir','is','it','je', + 'jm','jo','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','mk', + 'ml','mm','mn','mo', + 'mp','mq','mr','ms', + 'mt','mu','mv','mw', + 'mx','my','mz','na', + 'nc','ne','nf','ng', + 'ni','nl','no','np', + 'nr','nu','nz','om', + 'pa','pe','pf','pg', + 'ph','pk','pl','pm', + 'pn','pr','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','tf','tg', + 'th','tj','tk','tl', + 'tm','tn','to','tp', + 'tr','tt','tv','tw', + 'tz','ua','ug','uk', + 'us','uy','uz','va', + 'vc','ve','vg','vi', + 'vn','vu','wf','ws', + 'ye','yt','yu','za', + 'zm','zw', + ); + + /** + * Validate a tag URI (RFC4151) + * + * @param string $uri tag URI to validate + * + * @return boolean true if valid tag URI, false if not + * + * @access private + */ + function __uriRFC4151($uri) + { + $datevalid = false; + if (preg_match( + '/^tag:(?.*),(?\d{4}-?\d{0,2}-?\d{0,2}):(?.*)(.*:)*$/', $uri, $matches)) { + $date = $matches['date']; + $date6 = strtotime($date); + if ((strlen($date) == 4) && $date <= date('Y')) { + $datevalid = true; + } elseif ((strlen($date) == 7) && ($date6 < strtotime("now"))) { + $datevalid = true; + } elseif ((strlen($date) == 10) && ($date6 < strtotime("now"))) { + $datevalid = true; + } + if (self::email($matches['name'])) { + $namevalid = true; + } else { + $namevalid = self::email('info@' . $matches['name']); + } + return $datevalid && $namevalid; + } else { + return false; + } + } + + /** + * Validate a number + * + * @param string $number Number to validate + * @param array $options array where: + * 'decimal' is the decimal char or false when decimal + * not allowed. + * i.e. ',.' to allow both ',' and '.' + * 'dec_prec' Number of allowed decimals + * 'min' minimum value + * 'max' maximum value + * + * @return boolean true if valid number, false if not + * + * @access public + */ + function number($number, $options = array()) + { + $decimal = $dec_prec = $min = $max = null; + if (is_array($options)) { + extract($options); + } + + $dec_prec = $dec_prec ? "{1,$dec_prec}" : '+'; + $dec_regex = $decimal ? "[$decimal][0-9]$dec_prec" : ''; + + if (!preg_match("|^[-+]?\s*[0-9]+($dec_regex)?\$|", $number)) { + return false; + } + + if ($decimal != '.') { + $number = strtr($number, $decimal, '.'); + } + + $number = (float)str_replace(' ', '', $number); + if ($min !== null && $min > $number) { + return false; + } + + if ($max !== null && $max < $number) { + return false; + } + return true; + } + + /** + * Converting a string to UTF-7 (RFC 2152) + * + * @param string $string string to be converted + * + * @return string converted string + * + * @access private + */ + function __stringToUtf7($string) + { + $return = ''; + $utf7 = array( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', + 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', + '3', '4', '5', '6', '7', '8', '9', '+', ',' + ); + + + $state = 0; + + if (!empty($string)) { + $i = 0; + while ($i <= strlen($string)) { + $char = substr($string, $i, 1); + if ($state == 0) { + if ((ord($char) >= 0x7F) || (ord($char) <= 0x1F)) { + if ($char) { + $return .= '&'; + } + $state = 1; + } elseif ($char == '&') { + $return .= '&-'; + } else { + $return .= $char; + } + } elseif (($i == strlen($string) || + !((ord($char) >= 0x7F)) || (ord($char) <= 0x1F))) { + if ($state != 1) { + if (ord($char) > 64) { + $return .= ''; + } else { + $return .= $utf7[ord($char)]; + } + } + $return .= '-'; + $state = 0; + } else { + switch($state) { + case 1: + $return .= $utf7[ord($char) >> 2]; + $residue = (ord($char) & 0x03) << 4; + $state = 2; + break; + case 2: + $return .= $utf7[$residue | (ord($char) >> 4)]; + $residue = (ord($char) & 0x0F) << 2; + $state = 3; + break; + case 3: + $return .= $utf7[$residue | (ord($char) >> 6)]; + $return .= $utf7[ord($char) & 0x3F]; + $state = 1; + break; + } + } + $i++; + } + return $return; + } + return ''; + } + + /** + * Validate an email according to full RFC822 (inclusive human readable part) + * + * @param string $email email to validate, + * will return the address for optional dns validation + * @param array $options email() options + * + * @return boolean true if valid email, false if not + * + * @access private + */ + function __emailRFC822(&$email, &$options) + { + static $address = null; + static $uncomment = null; + if (!$address) { + // atom = 1* + $atom = '[^][()<>@,;:\\".\s\000-\037\177-\377]+\s*'; + // qtext = , ; => may be folded + // "\" & CR, and including linear-white-space> + $qtext = '[^"\\\\\r]'; + // quoted-pair = "\" CHAR ; may quote any char + $quoted_pair = '\\\\.'; + // quoted-string = <"> *(qtext/quoted-pair) <">; Regular qtext or + // ; quoted chars. + $quoted_string = '"(?:' . $qtext . '|' . $quoted_pair . ')*"\s*'; + // word = atom / quoted-string + $word = '(?:' . $atom . '|' . $quoted_string . ')'; + // local-part = word *("." word) ; uninterpreted + // ; case-preserved + $local_part = $word . '(?:\.\s*' . $word . ')*'; + // dtext = may be folded + // "]", "\" & CR, & including linear-white-space> + $dtext = '[^][\\\\\r]'; + // domain-literal = "[" *(dtext / quoted-pair) "]" + $domain_literal = '\[(?:' . $dtext . '|' . $quoted_pair . ')*\]\s*'; + // sub-domain = domain-ref / domain-literal + // domain-ref = atom ; symbolic reference + $sub_domain = '(?:' . $atom . '|' . $domain_literal . ')'; + // domain = sub-domain *("." sub-domain) + $domain = $sub_domain . '(?:\.\s*' . $sub_domain . ')*'; + // addr-spec = local-part "@" domain ; global address + $addr_spec = $local_part . '@\s*' . $domain; + // route = 1#("@" domain) ":" ; path-relative + $route = '@' . $domain . '(?:,@\s*' . $domain . ')*:\s*'; + // route-addr = "<" [route] addr-spec ">" + $route_addr = '<\s*(?:' . $route . ')?' . $addr_spec . '>\s*'; + // phrase = 1*word ; Sequence of words + $phrase = $word . '+'; + // mailbox = addr-spec ; simple address + // / phrase route-addr ; name & addr-spec + $mailbox = '(?:' . $addr_spec . '|' . $phrase . $route_addr . ')'; + // group = phrase ":" [#mailbox] ";" + $group = $phrase . ':\s*(?:' . $mailbox . '(?:,\s*' . $mailbox . ')*)?;\s*'; + // address = mailbox ; one addressee + // / group ; named list + $address = '/^\s*(?:' . $mailbox . '|' . $group . ')$/'; + + $uncomment = + '/((?:(?:\\\\"|[^("])*(?:' . $quoted_string . + ')?)*)((?{$tmpVar}; + } + + $e = $self->executeFullEmailValidation($email, $toValidate); + + return $e; + } + + /** + * Execute the validation + * + * This function will execute the full email vs tld + * validation using an array of tlds passed to it. + * + * @param string $email The email to validate. + * @param array $arrayOfTLDs The array of the TLDs to validate + * + * @access public + * + * @return true or false (Depending on if it validates or if it does not) + */ + function executeFullEmailValidation($email, $arrayOfTLDs) + { + $emailEnding = explode('.', $email); + $emailEnding = $emailEnding[count($emailEnding)-1]; + foreach ($arrayOfTLDs as $validator => $keys) { + if (in_array($emailEnding, $keys)) { + return true; + } + } + return false; + } + + /** + * Validate an email + * + * @param string $email email to validate + * @param mixed boolean (BC) $check_domain Check or not if the domain exists + * array $options associative array of options + * 'check_domain' boolean Check or not if the domain exists + * 'use_rfc822' boolean Apply the full RFC822 grammar + * + * Ex. + * $options = array( + * 'check_domain' => 'true', + * 'fullTLDValidation' => 'true', + * 'use_rfc822' => 'true', + * 'VALIDATE_GTLD_EMAILS' => 'true', + * 'VALIDATE_CCTLD_EMAILS' => 'true', + * 'VALIDATE_ITLD_EMAILS' => 'true', + * ); + * + * @return boolean true if valid email, false if not + * + * @access public + */ + function email($email, $options = null) + { + $check_domain = false; + $use_rfc822 = false; + if (is_bool($options)) { + $check_domain = $options; + } elseif (is_array($options)) { + extract($options); + } + + /** + * Check for IDN usage so we can encode the domain as Punycode + * before continuing. + */ + $hasIDNA = false; + + if (@include_once('Net/IDNA.php')) { + $hasIDNA = true; + } + + if ($hasIDNA === true) { + if (strpos($email, '@') !== false) { + list($name, $domain) = explode('@', $email, 2); + + // Check if the domain contains characters > 127 which means + // it's an idn domain name. + $chars = count_chars($domain, 1); + if (!empty($chars) && max(array_keys($chars)) > 127) { + $idna =& Net_IDNA::singleton(); + $domain = $idna->encode($domain); + } + + $email = "$name@$domain"; + } + } + + /** + * @todo Fix bug here.. even if it passes this, it won't be passing + * The regular expression below + */ + if (isset($fullTLDValidation)) { + //$valid = Validate::_fullTLDValidation($email, $fullTLDValidation); + $valid = Validate::_fullTLDValidation($email, $options); + + if (!$valid) { + return false; + } + } + + // the base regexp for address + $regex = '&^(?: # recipient: + ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| #1 quoted name + ([-\w!\#\$%\&\'*+~/^`|{}]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}]+)*)) #2 OR dot-atom + @(((\[)? #3 domain, 4 as IPv4, 5 optionally bracketed + (?:(?:(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:[0-1]?[0-9]?[0-9]))\.){3} + (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:[0-1]?[0-9]?[0-9]))))(?(5)\])| + ((?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*[a-z0-9](?:[-a-z0-9]*[a-z0-9])?) #6 domain as hostname + \.((?:([^- ])[-a-z]*[-a-z]))) #7 TLD + $&xi'; + + //checks if exists the domain (MX or A) + if ($use_rfc822? Validate::__emailRFC822($email, $options) : + preg_match($regex, $email)) { + if ($check_domain && function_exists('checkdnsrr')) { + list ($account, $domain) = explode('@', $email); + if (checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A')) { + return true; + } + return false; + } + return true; + } + return false; + } + + /** + * Validate a string using the given format 'format' + * + * @param string $string String to validate + * @param array $options Options array where: + * 'format' is the format of the string + * Ex:VALIDATE_NUM . VALIDATE_ALPHA (see constants) + * 'min_length' minimum length + * 'max_length' maximum length + * + * @return boolean true if valid string, false if not + * + * @access public + */ + function string($string, $options) + { + $format = null; + $min_length = 0; + $max_length = 0; + + if (is_array($options)) { + extract($options); + } + + if ($format && !preg_match("|^[$format]*\$|s", $string)) { + return false; + } + + if ($min_length && strlen($string) < $min_length) { + return false; + } + + if ($max_length && strlen($string) > $max_length) { + return false; + } + + return true; + } + + /** + * Validate an URI (RFC2396) + * This function will validate 'foobarstring' by default, to get it to validate + * only http, https, ftp and such you have to pass it in the allowed_schemes + * option, like this: + * + * $options = array('allowed_schemes' => array('http', 'https', 'ftp')) + * var_dump(Validate::uri('http://www.example.org', $options)); + * + * + * NOTE 1: The rfc2396 normally allows middle '-' in the top domain + * e.g. http://example.co-m should be valid + * However, as '-' is not used in any known TLD, it is invalid + * NOTE 2: As double shlashes // are allowed in the path part, only full URIs + * including an authority can be valid, no relative URIs + * the // are mandatory (optionally preceeded by the 'sheme:' ) + * NOTE 3: the full complience to rfc2396 is not achieved by default + * the characters ';/?:@$,' will not be accepted in the query part + * if not urlencoded, refer to the option "strict'" + * + * @param string $url URI to validate + * @param array $options Options used by the validation method. + * key => type + * 'domain_check' => boolean + * Whether to check the DNS entry or not + * 'allowed_schemes' => array, list of protocols + * List of allowed schemes ('http', + * 'ssh+svn', 'mms') + * 'strict' => string the refused chars + * in query and fragment parts + * default: ';/?:@$,' + * empty: accept all rfc2396 foreseen chars + * + * @return boolean true if valid uri, false if not + * + * @access public + */ + function uri($url, $options = null) + { + $strict = ';/?:@$,'; + $domain_check = false; + $allowed_schemes = null; + if (is_array($options)) { + extract($options); + } + if (is_array($allowed_schemes) && + in_array("tag", $allowed_schemes) + ) { + if (strpos($url, "tag:") === 0) { + return self::__uriRFC4151($url); + } + } + + if (preg_match( + '&^(?:([a-z][-+.a-z0-9]*):)? # 1. scheme + (?:// # authority start + (?:((?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'();:\&=+$,])*)@)? # 2. authority-userinfo + (?:((?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*[a-z](?:[a-z0-9]+)?\.?) # 3. authority-hostname OR + |([0-9]{1,3}(?:\.[0-9]{1,3}){3})) # 4. authority-ipv4 + (?::([0-9]*))?) # 5. authority-port + ((?:/(?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'():@\&=+$,;])*)*/?)? # 6. path + (?:\?([^#]*))? # 7. query + (?:\#((?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'();/?:@\&=+$,])*))? # 8. fragment + $&xi', $url, $matches)) { + $scheme = isset($matches[1]) ? $matches[1] : ''; + $authority = isset($matches[3]) ? $matches[3] : '' ; + if (is_array($allowed_schemes) && + !in_array($scheme, $allowed_schemes) + ) { + return false; + } + if (!empty($matches[4])) { + $parts = explode('.', $matches[4]); + foreach ($parts as $part) { + if ($part > 255) { + return false; + } + } + } elseif ($domain_check && function_exists('checkdnsrr')) { + if (!checkdnsrr($authority, 'A')) { + return false; + } + } + if ($strict) { + $strict = '#[' . preg_quote($strict, '#') . ']#'; + if ((!empty($matches[7]) && preg_match($strict, $matches[7])) + || (!empty($matches[8]) && preg_match($strict, $matches[8]))) { + return false; + } + } + return true; + } + return false; + } + + /** + * Validate date and times. Note that this method need the Date_Calc class + * + * @param string $date Date to validate + * @param array $options array options where : + * 'format' The format of the date (%d-%m-%Y) + * or rfc822_compliant + * 'min' The date has to be greater + * than this array($day, $month, $year) + * or PEAR::Date object + * 'max' The date has to be smaller than + * this array($day, $month, $year) + * or PEAR::Date object + * + * @return boolean true if valid date/time, false if not + * + * @access public + */ + function date($date, $options) + { + $max = false; + $min = false; + $format = ''; + + if (is_array($options)) { + extract($options); + } + + if (strtolower($format) == 'rfc822_compliant') { + $preg = '&^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),) \s+ + (?:(\d{2})?) \s+ + (?:(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)?) \s+ + (?:(\d{2}(\d{2})?)?) \s+ + (?:(\d{2}?)):(?:(\d{2}?))(:(?:(\d{2}?)))? \s+ + (?:[+-]\d{4}|UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[A-IK-Za-ik-z])$&xi'; + + if (!preg_match($preg, $date, $matches)) { + return false; + } + + $year = (int)$matches[4]; + $months = array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'); + $month = array_keys($months, $matches[3]); + $month = (int)$month[0]+1; + $day = (int)$matches[2]; + $weekday = $matches[1]; + $hour = (int)$matches[6]; + $minute = (int)$matches[7]; + isset($matches[9]) ? $second = (int)$matches[9] : $second = 0; + + if ((strlen($year) != 4) || + ($day > 31 || $day < 1)|| + ($hour > 23) || + ($minute > 59) || + ($second > 59)) { + return false; + } + } else { + $date_len = strlen($format); + for ($i = 0; $i < $date_len; $i++) { + $c = $format{$i}; + if ($c == '%') { + $next = $format{$i + 1}; + switch ($next) { + case 'j': + case 'd': + if ($next == 'j') { + $day = (int)Validate::_substr($date, 1, 2); + } else { + $day = (int)Validate::_substr($date, 0, 2); + } + if ($day < 1 || $day > 31) { + return false; + } + break; + case 'm': + case 'n': + if ($next == 'm') { + $month = (int)Validate::_substr($date, 0, 2); + } else { + $month = (int)Validate::_substr($date, 1, 2); + } + if ($month < 1 || $month > 12) { + return false; + } + break; + case 'Y': + case 'y': + if ($next == 'Y') { + $year = Validate::_substr($date, 4); + $year = (int)$year?$year:''; + } else { + $year = (int)(substr(date('Y'), 0, 2) . + Validate::_substr($date, 2)); + } + if (strlen($year) != 4 || $year < 0 || $year > 9999) { + return false; + } + break; + case 'g': + case 'h': + if ($next == 'g') { + $hour = Validate::_substr($date, 1, 2); + } else { + $hour = Validate::_substr($date, 2); + } + if (!preg_match('/^\d+$/', $hour) || $hour < 0 || $hour > 12) { + return false; + } + break; + case 'G': + case 'H': + if ($next == 'G') { + $hour = Validate::_substr($date, 1, 2); + } else { + $hour = Validate::_substr($date, 2); + } + if (!preg_match('/^\d+$/', $hour) || $hour < 0 || $hour > 24) { + return false; + } + break; + case 's': + case 'i': + $t = Validate::_substr($date, 2); + if (!preg_match('/^\d+$/', $t) || $t < 0 || $t > 59) { + return false; + } + break; + default: + trigger_error("Not supported char `$next' after % in offset " . ($i+2), E_USER_WARNING); + } + $i++; + } else { + //literal + if (Validate::_substr($date, 1) != $c) { + return false; + } + } + } + } + // there is remaing data, we don't want it + if (strlen($date) && (strtolower($format) != 'rfc822_compliant')) { + return false; + } + + if (isset($day) && isset($month) && isset($year)) { + if (!checkdate($month, $day, $year)) { + return false; + } + + if (strtolower($format) == 'rfc822_compliant') { + if ($weekday != date("D", mktime(0, 0, 0, $month, $day, $year))) { + return false; + } + } + + if ($min) { + include_once 'Date/Calc.php'; + if (is_a($min, 'Date') && + (Date_Calc::compareDates($day, $month, $year, + $min->getDay(), $min->getMonth(), $min->getYear()) < 0) + ) { + return false; + } elseif (is_array($min) && + (Date_Calc::compareDates($day, $month, $year, + $min[0], $min[1], $min[2]) < 0) + ) { + return false; + } + } + + if ($max) { + include_once 'Date/Calc.php'; + if (is_a($max, 'Date') && + (Date_Calc::compareDates($day, $month, $year, + $max->getDay(), $max->getMonth(), $max->getYear()) > 0) + ) { + return false; + } elseif (is_array($max) && + (Date_Calc::compareDates($day, $month, $year, + $max[0], $max[1], $max[2]) > 0) + ) { + return false; + } + } + } + + return true; + } + + /** + * Substr + * + * @param string &$date Date + * @param string $num Length + * @param string $opt Unknown + * + * @access private + * @return string + */ + function _substr(&$date, $num, $opt = false) + { + if ($opt && strlen($date) >= $opt && preg_match('/^[0-9]{'.$opt.'}/', $date, $m)) { + $ret = $m[0]; + } else { + $ret = substr($date, 0, $num); + } + $date = substr($date, strlen($ret)); + return $ret; + } + + function _modf($val, $div) + { + if (function_exists('bcmod')) { + return bcmod($val, $div); + } elseif (function_exists('fmod')) { + return fmod($val, $div); + } + $r = $val / $div; + $i = intval($r); + return intval($val - $i * $div + .1); + } + + /** + * Calculates sum of product of number digits with weights + * + * @param string $number number string + * @param array $weights reference to array of weights + * + * @access protected + * + * @return int returns product of number digits with weights + */ + function _multWeights($number, &$weights) + { + if (!is_array($weights)) { + return -1; + } + $sum = 0; + + $count = min(count($weights), strlen($number)); + if ($count == 0) { // empty string or weights array + return -1; + } + for ($i = 0; $i < $count; ++$i) { + $sum += intval(substr($number, $i, 1)) * $weights[$i]; + } + + return $sum; + } + + /** + * Calculates control digit for a given number + * + * @param string $number number string + * @param array $weights reference to array of weights + * @param int $modulo (optionsl) number + * @param int $subtract (optional) number + * @param bool $allow_high (optional) true if function can return number higher than 10 + * + * @access protected + * + * @return int -1 calculated control number is returned + */ + function _getControlNumber($number, &$weights, $modulo = 10, $subtract = 0, $allow_high = false) + { + // calc sum + $sum = Validate::_multWeights($number, $weights); + if ($sum == -1) { + return -1; + } + $mod = Validate::_modf($sum, $modulo); // calculate control digit + + if ($subtract > $mod && $mod > 0) { + $mod = $subtract - $mod; + } + if ($allow_high === false) { + $mod %= 10; // change 10 to zero + } + return $mod; + } + + /** + * Validates a number + * + * @param string $number number to validate + * @param array $weights reference to array of weights + * @param int $modulo (optional) number + * @param int $subtract (optional) number + * + * @access protected + * + * @return bool true if valid, false if not + */ + function _checkControlNumber($number, &$weights, $modulo = 10, $subtract = 0) + { + if (strlen($number) < count($weights)) { + return false; + } + $target_digit = substr($number, count($weights), 1); + $control_digit = Validate::_getControlNumber($number, $weights, $modulo, $subtract, $modulo > 10); + + if ($control_digit == -1) { + return false; + } + if ($target_digit === 'X' && $control_digit == 10) { + return true; + } + if ($control_digit != $target_digit) { + return false; + } + return true; + } + + /** + * Bulk data validation for data introduced in the form of an + * assoc array in the form $var_name => $value. + * Can be used on any of Validate subpackages + * + * @param array $data Ex: array('name' => 'toto', 'email' => 'toto@thing.info'); + * @param array $val_type Contains the validation type and all parameters used in. + * 'val_type' is not optional + * others validations properties must have the same name as the function + * parameters. + * Ex: array('toto'=>array('type'=>'string','format'='toto@thing.info','min_length'=>5)); + * @param boolean $remove if set, the elements not listed in data will be removed + * + * @return array value name => true|false the value name comes from the data key + * + * @access public + */ + function multiple(&$data, &$val_type, $remove = false) + { + $keys = array_keys($data); + $valid = array(); + + foreach ($keys as $var_name) { + if (!isset($val_type[$var_name])) { + if ($remove) { + unset($data[$var_name]); + } + continue; + } + $opt = $val_type[$var_name]; + $methods = get_class_methods('Validate'); + $val2check = $data[$var_name]; + // core validation method + if (in_array(strtolower($opt['type']), $methods)) { + //$opt[$opt['type']] = $data[$var_name]; + $method = $opt['type']; + unset($opt['type']); + + if (sizeof($opt) == 1 && is_array(reset($opt))) { + $opt = array_pop($opt); + } + $valid[$var_name] = call_user_func(array('Validate', $method), $val2check, $opt); + + /** + * external validation method in the form: + * "" + * Ex: us_ssn will include class Validate/US.php and call method ssn() + */ + } elseif (strpos($opt['type'], '_') !== false) { + $validateType = explode('_', $opt['type']); + $method = array_pop($validateType); + $class = implode('_', $validateType); + $classPath = str_replace('_', DIRECTORY_SEPARATOR, $class); + $class = 'Validate_' . $class; + if (!@include_once "Validate/$classPath.php") { + trigger_error("$class isn't installed or you may have some permissoin issues", E_USER_ERROR); + } + + $ce = substr(phpversion(), 0, 1) > 4 ? + class_exists($class, false) : class_exists($class); + if (!$ce || + !in_array($method, get_class_methods($class)) + ) { + trigger_error("Invalid validation type $class::$method", + E_USER_WARNING); + continue; + } + unset($opt['type']); + if (sizeof($opt) == 1) { + $opt = array_pop($opt); + } + $valid[$var_name] = call_user_func(array($class, $method), + $data[$var_name], $opt); + } else { + trigger_error("Invalid validation type {$opt['type']}", + E_USER_WARNING); + } + } + return $valid; + } +} + -- cgit v1.2.3-54-g00ecf From a5a7c5033fff28cfa69f1950116efc464d2acadc Mon Sep 17 00:00:00 2001 From: Robin Millette Date: Tue, 3 Feb 2009 21:36:11 +0000 Subject: Added PEAR Services/oEmbed and its dependencies for multimedia integration. --- README | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/README b/README index 0c605151b..9a2dfb757 100644 --- a/README +++ b/README @@ -26,16 +26,16 @@ instant messenger programs (GTalk/Jabber), and specially-designed desktop clients that support the Twitter API. Laconica supports an open standard called OpenMicroBlogging -(http://openmicroblogging.org/) that lets users on different Web sites + that lets users on different Web sites or in different companies subscribe to each others' notices. It enables a distributed social network spread all across the Web. Laconica was originally developed for the Open Software Service, -Identi.ca (http://identi.ca/). It is shared with you in hope that you +Identi.ca . It is shared with you in hope that you too make an Open Software Service available to your users. To learn -more, please see the Open Software Service Definition 1.0: +more, please see the Open Software Service Definition 1.1: - http://www.openknowledge.org/ossd + http://www.opendefinition.org/ossd License ======= @@ -182,6 +182,10 @@ and the URLs are listed here for your convenience. version may render your Laconica site unable to send or receive XMPP messages. - Facebook library. Used for the Facebook application. +- PEAR Services_oEmbed. Used for some multimedia integration. +- PEAR HTTP_Request is an oEmbed dependency. +- PEAR Validat is an oEmbed dependency.e +- PEAR Net_URL is an oEmbed dependency.2 A design goal of Laconica is that the basic Web functionality should work on even the most restrictive commercial hosting services. @@ -404,7 +408,7 @@ config.php, and access to the Laconica database from the mail server. XMPP ---- -XMPP (eXtended Message and Presence Protocol, http://xmpp.org/) is the +XMPP (eXtended Message and Presence Protocol, ) is the instant-messenger protocol that drives Jabber and GTalk IM. You can distribute messages via XMPP using the system below; however, you need to run the XMPP incoming daemon to allow incoming messages as @@ -537,7 +541,7 @@ Sample cron job: Sitemaps -------- -Sitemap files (http://sitemaps.org/) are a very nice way of telling +Sitemap files are a very nice way of telling search engines and other interested bots what's available on your site and what's changed recently. You can generate sitemap files for your Laconica instance. @@ -558,7 +562,7 @@ Laconica instance. like './sitemapindex.xml'. sitemap-directory is the directory where you want the sitemaps stored, like './sitemaps/' (make sure the dir exists). URL-prefix-for-sitemaps is the full URL for the sitemap dir, - typically something like 'http://example.net/mublog/sitemaps/'. + typically something like . You can use several methods for submitting your sitemap index to search engines to get your site indexed. One is to add a line like the @@ -612,7 +616,7 @@ modification to use the new output format. Translation ----------- -Translations in Laconica use the gettext system (http://www.gnu.org/software/gettext/). +Translations in Laconica use the gettext system . Theoretically, you can add your own sub-directory to the locale/ subdirectory to add a new language to your system. You'll need to compile the ".po" files into ".mo" files, however. @@ -627,7 +631,7 @@ Backups There is no built-in system for doing backups in Laconica. You can make backups of a working Laconica system by backing up the database and -the Web directory. To backup the database use mysqldump (http://ur1.ca/7xo) +the Web directory. To backup the database use mysqldump and to backup the Web directory, try tar. Private @@ -819,7 +823,7 @@ db -- This section is a reference to the configuration options for -DB_DataObject (see http://ur1.ca/7xp). The ones that you may want to +DB_DataObject (see ). The ones that you may want to set are listed below for clarity. database: a DSN (Data Source Name) for your Laconica database. This is @@ -919,7 +923,7 @@ server: If set, defines another server where avatars are stored in the the client to speed up page loading, either with another virtual server or with an NFS or SAMBA share. Clients typically only make 2 connections to a single server at a - time (http://ur1.ca/6ih), so this can parallelize the job. + time , so this can parallelize the job. Defaults to null. public @@ -1000,7 +1004,7 @@ memcached --------- You can get a significant boost in performance by caching some -database data in memcached (http://www.danga.com/memcached/). +database data in memcached . enabled: Set to true to enable. Default false. server: a string with the hostname of the memcached server. Can also @@ -1011,7 +1015,7 @@ sphinx You can get a significant boost in performance using Sphinx Search instead of your database server to search for users and notices. -(http://sphinxsearch.com/). +. enabled: Set to true to enable. Default false. server: a string with the hostname of the sphinx server. @@ -1024,7 +1028,7 @@ A catch-all for integration with other systems. source: The name to use for the source of posts to Twitter. Defaults to 'laconica', but if you request your own source name from - Twitter (http://twitter.com/help/request_source), you can use + Twitter , you can use that here instead. Status updates on Twitter will then have links to your site. @@ -1101,7 +1105,7 @@ Unstable version If you're adventurous or impatient, you may want to install the development version of Laconica. To get it, use the git version -control tool (http://git-scm.com/) like so: +control tool like so: git clone http://laconi.ca/software/laconica.git @@ -1114,7 +1118,7 @@ There are several ways to get more information about Laconica. * There is a mailing list for Laconica developers and admins at http://mail.laconi.ca/mailman/listinfo/laconica-dev -* The #laconica IRC channel on freenode.net (http://www.freenode.net/). +* The #laconica IRC channel on freenode.net . * The Laconica wiki, http://laconi.ca/trac/ Feedback -- cgit v1.2.3-54-g00ecf